main
js 150 lines 4.92 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 */
9
10 import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
11 import type {HooksNode, HooksTree} from 'react-debug-tools/src/ReactDebugHooks';
12 import type {WorkTagMap} from '../../types';
13
14 import {getFiberFlags} from './DevToolsFiberInspection';
15 import is from 'shared/objectIs';
16
17 export function getContextChanged(prevFiber: Fiber, nextFiber: Fiber): boolean {
18 let prevContext =
19 prevFiber.dependencies && prevFiber.dependencies.firstContext;
20 let nextContext =
21 nextFiber.dependencies && nextFiber.dependencies.firstContext;
22
23 while (prevContext && nextContext) {
24 // Note this only works for versions of React that support this key (e.v. 18+)
25 // For older versions, there's no good way to read the current context value after render has completed.
26 // This is because React maintains a stack of context values during render,
27 // but by the time DevTools is called, render has finished and the stack is empty.
28 if (prevContext.context !== nextContext.context) {
29 // If the order of context has changed, then the later context values might have
30 // changed too but the main reason it rerendered was earlier. Either an earlier
31 // context changed value but then we would have exited already. If we end up here
32 // it's because a state or props change caused the order of contexts used to change.
33 // So the main cause is not the contexts themselves.
34 return false;
35 }
36 if (!is(prevContext.memoizedValue, nextContext.memoizedValue)) {
37 return true;
38 }
39
40 prevContext = prevContext.next;
41 nextContext = nextContext.next;
42 }
43 return false;
44 }
45
46 export function didStatefulHookChange(
47 prev: HooksNode,
48 next: HooksNode,
49 ): boolean {
50 // Detect the shape of useState() / useReducer() / useTransition() / useSyncExternalStore() / useActionState()
51 const isStatefulHook =
52 prev.isStateEditable === true ||
53 prev.name === 'SyncExternalStore' ||
54 prev.name === 'Transition' ||
55 prev.name === 'ActionState' ||
56 prev.name === 'FormState';
57
58 // Compare the values to see if they changed
59 if (isStatefulHook) {
60 return prev.value !== next.value;
61 }
62
63 return false;
64 }
65
66 export function getChangedHooksIndices(
67 prevHooks: HooksTree | null,
68 nextHooks: HooksTree | null,
69 ): null | Array<number> {
70 if (prevHooks == null || nextHooks == null) {
71 return null;
72 }
73
74 const indices: Array<number> = [];
75 let index = 0;
76
77 function traverse(prevTree: HooksTree, nextTree: HooksTree): void {
78 for (let i = 0; i < prevTree.length; i++) {
79 const prevHook = prevTree[i];
80 const nextHook = nextTree[i];
81
82 if (prevHook.subHooks.length > 0 && nextHook.subHooks.length > 0) {
83 traverse(prevHook.subHooks, nextHook.subHooks);
84 continue;
85 }
86
87 if (didStatefulHookChange(prevHook, nextHook)) {
88 indices.push(index);
89 }
90
91 index++;
92 }
93 }
94
95 traverse(prevHooks, nextHooks);
96 return indices;
97 }
98
99 export function getChangedKeys(prev: any, next: any): null | Array<string> {
100 if (prev == null || next == null) {
101 return null;
102 }
103
104 const keys = new Set([...Object.keys(prev), ...Object.keys(next)]);
105 const changedKeys = [];
106 // eslint-disable-next-line no-for-of-loops/no-for-of-loops
107 for (const key of keys) {
108 if (prev[key] !== next[key]) {
109 changedKeys.push(key);
110 }
111 }
112
113 return changedKeys;
114 }
115
116 /**
117 * Returns true iff nextFiber actually performed any work and produced an update.
118 * For generic components, like Function or Class components, prevFiber is not considered.
119 */
120 export function didFiberRender(
121 workTagMap: WorkTagMap,
122 prevFiber: Fiber,
123 nextFiber: Fiber,
124 ): boolean {
125 switch (nextFiber.tag) {
126 case workTagMap.ClassComponent:
127 case workTagMap.FunctionComponent:
128 case workTagMap.ContextConsumer:
129 case workTagMap.MemoComponent:
130 case workTagMap.SimpleMemoComponent:
131 case workTagMap.ForwardRef:
132 // For types that execute user code, we check PerformedWork effect.
133 // We don't reflect bailouts (either referential or sCU) in DevTools.
134 // TODO: This flag is a leaked implementation detail. Once we start
135 // releasing DevTools in lockstep with React, we should import a
136 // function from the reconciler instead.
137 const PerformedWork = 0b000000000000000000000000001;
138 return (getFiberFlags(nextFiber) & PerformedWork) === PerformedWork;
139 // Note: ContextConsumer only gets PerformedWork effect in 16.3.3+
140 // so it won't get highlighted with React 16.3.0 to 16.3.2.
141 default:
142 // For host components and other types, we compare inputs
143 // to determine whether something is an update.
144 return (
145 prevFiber.memoizedProps !== nextFiber.memoizedProps ||
146 prevFiber.memoizedState !== nextFiber.memoizedState ||
147 prevFiber.ref !== nextFiber.ref
148 );
149 }
150 }