/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow */ import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; import type {HooksNode, HooksTree} from 'react-debug-tools/src/ReactDebugHooks'; import type {WorkTagMap} from '../../types'; import {getFiberFlags} from './DevToolsFiberInspection'; import is from 'shared/objectIs'; export function getContextChanged(prevFiber: Fiber, nextFiber: Fiber): boolean { let prevContext = prevFiber.dependencies && prevFiber.dependencies.firstContext; let nextContext = nextFiber.dependencies && nextFiber.dependencies.firstContext; while (prevContext && nextContext) { // Note this only works for versions of React that support this key (e.v. 18+) // For older versions, there's no good way to read the current context value after render has completed. // This is because React maintains a stack of context values during render, // but by the time DevTools is called, render has finished and the stack is empty. if (prevContext.context !== nextContext.context) { // If the order of context has changed, then the later context values might have // changed too but the main reason it rerendered was earlier. Either an earlier // context changed value but then we would have exited already. If we end up here // it's because a state or props change caused the order of contexts used to change. // So the main cause is not the contexts themselves. return false; } if (!is(prevContext.memoizedValue, nextContext.memoizedValue)) { return true; } prevContext = prevContext.next; nextContext = nextContext.next; } return false; } export function didStatefulHookChange( prev: HooksNode, next: HooksNode, ): boolean { // Detect the shape of useState() / useReducer() / useTransition() / useSyncExternalStore() / useActionState() const isStatefulHook = prev.isStateEditable === true || prev.name === 'SyncExternalStore' || prev.name === 'Transition' || prev.name === 'ActionState' || prev.name === 'FormState'; // Compare the values to see if they changed if (isStatefulHook) { return prev.value !== next.value; } return false; } export function getChangedHooksIndices( prevHooks: HooksTree | null, nextHooks: HooksTree | null, ): null | Array { if (prevHooks == null || nextHooks == null) { return null; } const indices: Array = []; let index = 0; function traverse(prevTree: HooksTree, nextTree: HooksTree): void { for (let i = 0; i < prevTree.length; i++) { const prevHook = prevTree[i]; const nextHook = nextTree[i]; if (prevHook.subHooks.length > 0 && nextHook.subHooks.length > 0) { traverse(prevHook.subHooks, nextHook.subHooks); continue; } if (didStatefulHookChange(prevHook, nextHook)) { indices.push(index); } index++; } } traverse(prevHooks, nextHooks); return indices; } export function getChangedKeys(prev: any, next: any): null | Array { if (prev == null || next == null) { return null; } const keys = new Set([...Object.keys(prev), ...Object.keys(next)]); const changedKeys = []; // eslint-disable-next-line no-for-of-loops/no-for-of-loops for (const key of keys) { if (prev[key] !== next[key]) { changedKeys.push(key); } } return changedKeys; } /** * Returns true iff nextFiber actually performed any work and produced an update. * For generic components, like Function or Class components, prevFiber is not considered. */ export function didFiberRender( workTagMap: WorkTagMap, prevFiber: Fiber, nextFiber: Fiber, ): boolean { switch (nextFiber.tag) { case workTagMap.ClassComponent: case workTagMap.FunctionComponent: case workTagMap.ContextConsumer: case workTagMap.MemoComponent: case workTagMap.SimpleMemoComponent: case workTagMap.ForwardRef: // For types that execute user code, we check PerformedWork effect. // We don't reflect bailouts (either referential or sCU) in DevTools. // TODO: This flag is a leaked implementation detail. Once we start // releasing DevTools in lockstep with React, we should import a // function from the reconciler instead. const PerformedWork = 0b000000000000000000000000001; return (getFiberFlags(nextFiber) & PerformedWork) === PerformedWork; // Note: ContextConsumer only gets PerformedWork effect in 16.3.3+ // so it won't get highlighted with React 16.3.0 to 16.3.2. default: // For host components and other types, we compare inputs // to determine whether something is an update. return ( prevFiber.memoizedProps !== nextFiber.memoizedProps || prevFiber.memoizedState !== nextFiber.memoizedState || prevFiber.ref !== nextFiber.ref ); } }