From c6a3e4d3725abe1a29d3c7fbdeb14f0ab7c57e35 Mon Sep 17 00:00:00 2001 From: Jack Pope Date: Wed, 10 Jul 2024 17:18:01 -0400 Subject: [PATCH 1/6] Add unstable context access API for internal profiling --- .../react-debug-tools/src/ReactDebugHooks.js | 2 +- .../react-reconciler/src/ReactFiberHooks.js | 89 +++++++- .../src/ReactFiberNewContext.js | 80 ++++++- .../src/ReactInternalTypes.js | 16 +- .../__tests__/ReactContextWithBailout-test.js | 210 ++++++++++++++++++ packages/react/index.fb.js | 1 + packages/react/src/ReactClient.js | 2 + packages/react/src/ReactHooks.js | 25 +++ packages/shared/ReactFeatureFlags.js | 3 + .../forks/ReactFeatureFlags.native-fb.js | 1 + .../forks/ReactFeatureFlags.native-oss.js | 1 + .../forks/ReactFeatureFlags.test-renderer.js | 1 + ...actFeatureFlags.test-renderer.native-fb.js | 1 + .../ReactFeatureFlags.test-renderer.www.js | 1 + .../shared/forks/ReactFeatureFlags.www.js | 2 + 15 files changed, 421 insertions(+), 14 deletions(-) create mode 100644 packages/react-reconciler/src/__tests__/ReactContextWithBailout-test.js diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js index 69827f2bf05f3..1ecc02aa3e652 100644 --- a/packages/react-debug-tools/src/ReactDebugHooks.js +++ b/packages/react-debug-tools/src/ReactDebugHooks.js @@ -155,7 +155,7 @@ function getPrimitiveStackCache(): Map> { let currentFiber: null | Fiber = null; let currentHook: null | Hook = null; -let currentContextDependency: null | ContextDependency = null; +let currentContextDependency: null | ContextDependency = null; function nextHook(): null | Hook { const hook = currentHook; diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index 5e9d9085457c1..a2567bd64118e 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -47,6 +47,7 @@ import { enableUseDeferredValueInitialArg, disableLegacyMode, enableNoCloningMemoCache, + enableContextProfiling, } from 'shared/ReactFeatureFlags'; import { REACT_CONTEXT_TYPE, @@ -81,7 +82,11 @@ import { ContinuousEventPriority, higherEventPriority, } from './ReactEventPriorities'; -import {readContext, checkIfContextChanged} from './ReactFiberNewContext'; +import { + readContext, + readContextAndCompare, + checkIfContextChanged, +} from './ReactFiberNewContext'; import {HostRoot, CacheComponent, HostComponent} from './ReactWorkTags'; import { LayoutStatic as LayoutStaticEffect, @@ -1053,6 +1058,13 @@ function updateWorkInProgressHook(): Hook { return workInProgressHook; } +function unstable_useContextWithBailout( + context: ReactContext, + compare: void | (T => mixed), +): T { + return readContextAndCompare(context, compare); +} + // NOTE: defining two versions of this function to avoid size impact when this feature is disabled. // Previously this function was inlined, the additional `memoCache` property makes it not inlined. let createFunctionComponentUpdateQueue: () => FunctionComponentUpdateQueue; @@ -3689,6 +3701,10 @@ if (enableAsyncActions) { if (enableAsyncActions) { (ContextOnlyDispatcher: Dispatcher).useOptimistic = throwInvalidHookError; } +if (enableContextProfiling) { + (ContextOnlyDispatcher: Dispatcher).unstable_useContextWithBailout = + throwInvalidHookError; +} const HooksDispatcherOnMount: Dispatcher = { readContext, @@ -3728,6 +3744,10 @@ if (enableAsyncActions) { if (enableAsyncActions) { (HooksDispatcherOnMount: Dispatcher).useOptimistic = mountOptimistic; } +if (enableContextProfiling) { + (HooksDispatcherOnMount: Dispatcher).unstable_useContextWithBailout = + unstable_useContextWithBailout; +} const HooksDispatcherOnUpdate: Dispatcher = { readContext, @@ -3767,6 +3787,10 @@ if (enableAsyncActions) { if (enableAsyncActions) { (HooksDispatcherOnUpdate: Dispatcher).useOptimistic = updateOptimistic; } +if (enableContextProfiling) { + (HooksDispatcherOnUpdate: Dispatcher).unstable_useContextWithBailout = + unstable_useContextWithBailout; +} const HooksDispatcherOnRerender: Dispatcher = { readContext, @@ -3806,6 +3830,10 @@ if (enableAsyncActions) { if (enableAsyncActions) { (HooksDispatcherOnRerender: Dispatcher).useOptimistic = rerenderOptimistic; } +if (enableContextProfiling) { + (HooksDispatcherOnRerender: Dispatcher).unstable_useContextWithBailout = + unstable_useContextWithBailout; +} let HooksDispatcherOnMountInDEV: Dispatcher | null = null; let HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher | null = null; @@ -4019,6 +4047,14 @@ if (__DEV__) { return mountOptimistic(passthrough, reducer); }; } + if (enableContextProfiling) { + (HooksDispatcherOnMountInDEV: Dispatcher).unstable_useContextWithBailout = + function (context: ReactContext, compare: void | (T => mixed)): T { + currentHookNameInDev = 'useContext'; + mountHookTypesDev(); + return unstable_useContextWithBailout(context, compare); + }; + } HooksDispatcherOnMountWithHookTypesInDEV = { readContext(context: ReactContext): T { @@ -4200,6 +4236,14 @@ if (__DEV__) { return mountOptimistic(passthrough, reducer); }; } + if (enableContextProfiling) { + (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).unstable_useContextWithBailout = + function (context: ReactContext, compare: void | (T => mixed)): T { + currentHookNameInDev = 'useContext'; + updateHookTypesDev(); + return unstable_useContextWithBailout(context, compare); + }; + } HooksDispatcherOnUpdateInDEV = { readContext(context: ReactContext): T { @@ -4380,6 +4424,14 @@ if (__DEV__) { return updateOptimistic(passthrough, reducer); }; } + if (enableContextProfiling) { + (HooksDispatcherOnUpdateInDEV: Dispatcher).unstable_useContextWithBailout = + function (context: ReactContext, compare: void | (T => mixed)): T { + currentHookNameInDev = 'useContext'; + updateHookTypesDev(); + return unstable_useContextWithBailout(context, compare); + }; + } HooksDispatcherOnRerenderInDEV = { readContext(context: ReactContext): T { @@ -4560,6 +4612,14 @@ if (__DEV__) { return rerenderOptimistic(passthrough, reducer); }; } + if (enableContextProfiling) { + (HooksDispatcherOnUpdateInDEV: Dispatcher).unstable_useContextWithBailout = + function (context: ReactContext, compare: void | (T => mixed)): T { + currentHookNameInDev = 'useContext'; + updateHookTypesDev(); + return unstable_useContextWithBailout(context, compare); + }; + } InvalidNestedHooksDispatcherOnMountInDEV = { readContext(context: ReactContext): T { @@ -4766,6 +4826,15 @@ if (__DEV__) { return mountOptimistic(passthrough, reducer); }; } + if (enableContextProfiling) { + (HooksDispatcherOnUpdateInDEV: Dispatcher).unstable_useContextWithBailout = + function (context: ReactContext, compare: void | (T => mixed)): T { + currentHookNameInDev = 'useContext'; + warnInvalidHookAccess(); + mountHookTypesDev(); + return unstable_useContextWithBailout(context, compare); + }; + } InvalidNestedHooksDispatcherOnUpdateInDEV = { readContext(context: ReactContext): T { @@ -4972,6 +5041,15 @@ if (__DEV__) { return updateOptimistic(passthrough, reducer); }; } + if (enableContextProfiling) { + (InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).unstable_useContextWithBailout = + function (context: ReactContext, compare: void | (T => mixed)): T { + currentHookNameInDev = 'useContext'; + warnInvalidHookAccess(); + updateHookTypesDev(); + return unstable_useContextWithBailout(context, compare); + }; + } InvalidNestedHooksDispatcherOnRerenderInDEV = { readContext(context: ReactContext): T { @@ -5178,4 +5256,13 @@ if (__DEV__) { return rerenderOptimistic(passthrough, reducer); }; } + if (enableContextProfiling) { + (InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).unstable_useContextWithBailout = + function (context: ReactContext, compare: void | (T => mixed)): T { + currentHookNameInDev = 'useContext'; + warnInvalidHookAccess(); + updateHookTypesDev(); + return unstable_useContextWithBailout(context, compare); + }; + } } diff --git a/packages/react-reconciler/src/ReactFiberNewContext.js b/packages/react-reconciler/src/ReactFiberNewContext.js index c19e056b0a3cd..bec21a08686fe 100644 --- a/packages/react-reconciler/src/ReactFiberNewContext.js +++ b/packages/react-reconciler/src/ReactFiberNewContext.js @@ -51,6 +51,8 @@ import { getHostTransitionProvider, HostTransitionContext, } from './ReactFiberHostContext'; +import isArray from '../../shared/isArray'; +import {enableContextProfiling} from '../../shared/ReactFeatureFlags'; const valueCursor: StackCursor = createCursor(null); @@ -70,7 +72,7 @@ if (__DEV__) { } let currentlyRenderingFiber: Fiber | null = null; -let lastContextDependency: ContextDependency | null = null; +let lastContextDependency: ContextDependency | null = null; let lastFullyObservedContext: ReactContext | null = null; let isDisallowedContextReadInDEV: boolean = false; @@ -400,8 +402,22 @@ function propagateContextChanges( findContext: for (let i = 0; i < contexts.length; i++) { const context: ReactContext = contexts[i]; // Check if the context matches. - // TODO: Compare selected values to bail out early. if (dependency.context === context) { + const compare = dependency.compare; + if (enableContextProfiling && compare != null) { + const newValue = isPrimaryRenderer + ? dependency.context._currentValue + : dependency.context._currentValue2; + if ( + !checkIfComparedContextValuesChanged( + dependency.lastComparedValue, + compare(newValue), + ) + ) { + // Compared value hasn't changed. Bail out early. + continue findContext; + } + } // Match! Schedule an update on this fiber. // In the lazy implementation, don't mark a dirty flag on the @@ -641,6 +657,28 @@ function propagateParentContextChanges( workInProgress.flags |= DidPropagateContext; } +function checkIfComparedContextValuesChanged( + oldComparedValue: mixed, + newComparedValue: mixed, +): boolean { + if (isArray(oldComparedValue) && isArray(newComparedValue)) { + for ( + let i = 0; + i < oldComparedValue.length && i < newComparedValue.length; + i++ + ) { + if (!is(newComparedValue[i], oldComparedValue[i])) { + return true; + } + } + } else { + if (!is(newComparedValue, oldComparedValue)) { + return true; + } + } + return false; +} + export function checkIfContextChanged( currentDependencies: Dependencies, ): boolean { @@ -659,8 +697,20 @@ export function checkIfContextChanged( ? context._currentValue : context._currentValue2; const oldValue = dependency.memoizedValue; - if (!is(newValue, oldValue)) { - return true; + const compare = dependency.compare; + if (enableContextProfiling && compare != null) { + if ( + checkIfComparedContextValuesChanged( + dependency.lastComparedValue, + compare(newValue), + ) + ) { + return true; + } + } else { + if (!is(newValue, oldValue)) { + return true; + } } dependency = dependency.next; } @@ -694,6 +744,17 @@ export function prepareToReadContext( } } +export function readContextAndCompare( + context: ReactContext, + compare: void | (C => mixed), +): C { + if (!enableLazyContextPropagation) { + return readContext(context); + } + + return readContextForConsumer(currentlyRenderingFiber, context, compare); +} + export function readContext(context: ReactContext): T { if (__DEV__) { // This warning would fire if you read context inside a Hook like useMemo. @@ -721,10 +782,13 @@ export function readContextDuringReconciliation( return readContextForConsumer(consumer, context); } -function readContextForConsumer( +type ContextCompare = C => S; + +function readContextForConsumer( consumer: Fiber | null, - context: ReactContext, -): T { + context: ReactContext, + compare?: void | (C => S), +): C { const value = isPrimaryRenderer ? context._currentValue : context._currentValue2; @@ -736,6 +800,8 @@ function readContextForConsumer( context: ((context: any): ReactContext), memoizedValue: value, next: null, + compare: ((compare: any): ContextCompare | null), + lastComparedValue: compare != null ? compare(value) : null, }; if (lastContextDependency === null) { diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js index beee88de2549b..67c260d78a0e9 100644 --- a/packages/react-reconciler/src/ReactInternalTypes.js +++ b/packages/react-reconciler/src/ReactInternalTypes.js @@ -61,16 +61,18 @@ export type HookType = | 'useFormState' | 'useActionState'; -export type ContextDependency = { - context: ReactContext, - next: ContextDependency | null, - memoizedValue: T, +export type ContextDependency = { + context: ReactContext, + next: ContextDependency | null, + memoizedValue: C, + compare: (C => S) | null, + lastComparedValue: S | null, ... }; export type Dependencies = { lanes: Lanes, - firstContext: ContextDependency | null, + firstContext: ContextDependency | null, ... }; @@ -384,6 +386,10 @@ export type Dispatcher = { initialArg: I, init?: (I) => S, ): [S, Dispatch], + unstable_useContextWithBailout?: ( + context: ReactContext, + compare: void | (T => mixed), + ) => T, useContext(context: ReactContext): T, useRef(initialValue: T): {current: T}, useEffect( diff --git a/packages/react-reconciler/src/__tests__/ReactContextWithBailout-test.js b/packages/react-reconciler/src/__tests__/ReactContextWithBailout-test.js new file mode 100644 index 0000000000000..5150e9240d3fe --- /dev/null +++ b/packages/react-reconciler/src/__tests__/ReactContextWithBailout-test.js @@ -0,0 +1,210 @@ +let React; +let ReactNoop; +let Scheduler; +let act; +let assertLog; +let useState; +let useContext; +let unstable_useContextWithBailout; + +describe('ReactContextWithBailout', () => { + beforeEach(() => { + jest.resetModules(); + + React = require('react'); + ReactNoop = require('react-noop-renderer'); + Scheduler = require('scheduler'); + const testUtils = require('internal-test-utils'); + act = testUtils.act; + assertLog = testUtils.assertLog; + useState = React.useState; + useContext = React.useContext; + unstable_useContextWithBailout = React.unstable_useContextWithBailout; + }); + + function Text({text}) { + Scheduler.log(text); + return text; + } + + // @gate enableLazyContextPropagation && enableContextProfiling + test('unstable_useContextWithBailout basic usage', async () => { + const Context = React.createContext(); + + let setContext; + function App() { + const [context, _setContext] = useState({a: 'A0', b: 'B0', c: 'C0'}); + setContext = _setContext; + return ( + + + + ); + } + + // Intermediate parent that bails out. Children will only re-render when the + // context changes. + const Indirection = React.memo(() => { + return ( + <> + A: , B: , C: , AB: + + ); + }); + + function A() { + const {a} = unstable_useContextWithBailout(Context, context => context.a); + return ; + } + + function B() { + const {b} = unstable_useContextWithBailout(Context, context => context.b); + return ; + } + + function C() { + const {c} = unstable_useContextWithBailout(Context, context => context.c); + return ; + } + + function AB() { + const {a, b} = unstable_useContextWithBailout(Context, context => [ + context.a, + context.b, + ]); + return ; + } + + const root = ReactNoop.createRoot(); + await act(async () => { + root.render(); + }); + assertLog(['A0', 'B0', 'C0', 'A0B0']); + expect(root).toMatchRenderedOutput('A: A0, B: B0, C: C0, AB: A0B0'); + + // Update a. Only the A and AB consumer should re-render. + await act(async () => { + setContext({a: 'A1', c: 'C0', b: 'B0'}); + }); + assertLog(['A1', 'A1B0']); + expect(root).toMatchRenderedOutput('A: A1, B: B0, C: C0, AB: A1B0'); + + // Update b. Only the B and AB consumer should re-render. + await act(async () => { + setContext({a: 'A1', b: 'B1', c: 'C0'}); + }); + assertLog(['B1', 'A1B1']); + expect(root).toMatchRenderedOutput('A: A1, B: B1, C: C0, AB: A1B1'); + + // Update c. Only the C consumer should re-render. + await act(async () => { + setContext({a: 'A1', b: 'B1', c: 'C1'}); + }); + assertLog(['C1']); + expect(root).toMatchRenderedOutput('A: A1, B: B1, C: C1, AB: A1B1'); + }); + + // @gate enableLazyContextPropagation && enableContextProfiling + test('unstable_useContextWithBailout and useContext subscribing to same context in same component', async () => { + const Context = React.createContext(); + + let setContext; + function App() { + const [context, _setContext] = useState({a: 0, b: 0, unrelated: 0}); + setContext = _setContext; + return ( + + + + ); + } + + // Intermediate parent that bails out. Children will only re-render when the + // context changes. + const Indirection = React.memo(() => { + return ; + }); + + function Child() { + const {a} = unstable_useContextWithBailout(Context, context => context.a); + const context = useContext(Context); + return ; + } + + const root = ReactNoop.createRoot(); + await act(async () => { + root.render(); + }); + assertLog(['A: 0, B: 0']); + expect(root).toMatchRenderedOutput('A: 0, B: 0'); + + // Update an unrelated field that isn't used by the component. The context + // attempts to bail out, but the normal context forces an update. + await act(async () => { + setContext({a: 0, b: 0, unrelated: 1}); + }); + assertLog(['A: 0, B: 0']); + expect(root).toMatchRenderedOutput('A: 0, B: 0'); + }); + + // @gate enableLazyContextPropagation && enableContextProfiling + test('unstable_useContextWithBailout and useContext subscribing to different contexts in same component', async () => { + const ContextA = React.createContext(); + const ContextB = React.createContext(); + + let setContextA; + let setContextB; + function App() { + const [a, _setContextA] = useState({a: 0, unrelated: 0}); + const [b, _setContextB] = useState(0); + setContextA = _setContextA; + setContextB = _setContextB; + return ( + + + + + + ); + } + + // Intermediate parent that bails out. Children will only re-render when the + // context changes. + const Indirection = React.memo(() => { + return ; + }); + + function Child() { + const {a} = unstable_useContextWithBailout( + ContextA, + context => context.a, + ); + const b = useContext(ContextB); + return ; + } + + const root = ReactNoop.createRoot(); + await act(async () => { + root.render(); + }); + assertLog(['A: 0, B: 0']); + expect(root).toMatchRenderedOutput('A: 0, B: 0'); + + // Update a field in A that isn't part of the compared context. It should + // bail out. + await act(async () => { + setContextA({a: 0, unrelated: 1}); + }); + assertLog([]); + expect(root).toMatchRenderedOutput('A: 0, B: 0'); + + // Now update the same a field again, but this time, also update a different + // context in the same batch. The other context prevents a bail out. + await act(async () => { + setContextA({a: 0, unrelated: 1}); + setContextB(1); + }); + assertLog(['A: 0, B: 1']); + expect(root).toMatchRenderedOutput('A: 0, B: 1'); + }); +}); diff --git a/packages/react/index.fb.js b/packages/react/index.fb.js index 49259d9eaf50f..1b87e4b2e582f 100644 --- a/packages/react/index.fb.js +++ b/packages/react/index.fb.js @@ -39,6 +39,7 @@ export { use, useActionState, useCallback, + unstable_useContextWithBailout, useContext, useDebugValue, useDeferredValue, diff --git a/packages/react/src/ReactClient.js b/packages/react/src/ReactClient.js index fc668246a988b..318d8e648d9d5 100644 --- a/packages/react/src/ReactClient.js +++ b/packages/react/src/ReactClient.js @@ -38,6 +38,7 @@ import {postpone} from './ReactPostpone'; import { getCacheForType, useCallback, + unstable_useContextWithBailout, useContext, useEffect, useEffectEvent, @@ -83,6 +84,7 @@ export { cache, postpone as unstable_postpone, useCallback, + unstable_useContextWithBailout, useContext, useEffect, useEffectEvent as experimental_useEffectEvent, diff --git a/packages/react/src/ReactHooks.js b/packages/react/src/ReactHooks.js index 93d9fa28f07f9..697752ef9698e 100644 --- a/packages/react/src/ReactHooks.js +++ b/packages/react/src/ReactHooks.js @@ -19,6 +19,10 @@ import {REACT_CONSUMER_TYPE} from 'shared/ReactSymbols'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import {enableAsyncActions} from 'shared/ReactFeatureFlags'; +import { + enableContextProfiling, + enableLazyContextPropagation, +} from '../../shared/ReactFeatureFlags'; type BasicStateAction = (S => S) | S; type Dispatch = A => void; @@ -65,6 +69,27 @@ export function useContext(Context: ReactContext): T { return dispatcher.useContext(Context); } +export function unstable_useContextWithBailout( + context: ReactContext, + compare: void | (T => mixed), +): T { + if (!(enableLazyContextPropagation && enableContextProfiling)) { + throw new Error('Not implemented.'); + } + + const dispatcher = resolveDispatcher(); + if (__DEV__) { + if (context.$$typeof === REACT_CONSUMER_TYPE) { + console.error( + 'Calling useContext(Context.Consumer) is not supported and will cause bugs. ' + + 'Did you mean to call useContext(Context) instead?', + ); + } + } + // $FlowFixMe[not-a-function] This is unstable, thus optional + return dispatcher.unstable_useContextWithBailout(context, compare); +} + export function useState( initialState: (() => S) | S, ): [S, Dispatch>] { diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 2e7c9ffea4952..7e62beddb30cc 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -97,6 +97,9 @@ export const enableTransitionTracing = false; // No known bugs, but needs performance testing export const enableLazyContextPropagation = false; +// Expose unstable useContext for performance testing +export const enableContextProfiling = false; + // FB-only usage. The new API has different semantics. export const enableLegacyHidden = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 8a5f784ea943f..77b6da8ef8514 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -58,6 +58,7 @@ export const enableFlightReadableStream = true; export const enableGetInspectorDataForInstanceInProduction = true; export const enableInfiniteRenderLoopDetection = true; export const enableLazyContextPropagation = false; +export const enableContextProfiling = false; export const enableLegacyCache = false; export const enableLegacyFBSupport = false; export const enableLegacyHidden = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index e1780bf019faa..f463ab907c069 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -51,6 +51,7 @@ export const enableFlightReadableStream = true; export const enableGetInspectorDataForInstanceInProduction = false; export const enableInfiniteRenderLoopDetection = true; export const enableLazyContextPropagation = false; +export const enableContextProfiling = false; export const enableLegacyCache = false; export const enableLegacyFBSupport = false; export const enableLegacyHidden = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index 4208674d503be..e1a2d5db92fa5 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -52,6 +52,7 @@ export const transitionLaneExpirationMs = 5000; export const disableSchedulerTimeoutInWorkLoop = false; export const enableLazyContextPropagation = false; +export const enableContextProfiling = false; export const enableLegacyHidden = false; export const forceConcurrentByDefaultForTesting = false; export const allowConcurrentByDefault = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js index f23cdfe8136d3..b2b863d8ec565 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js @@ -43,6 +43,7 @@ export const enableFlightReadableStream = true; export const enableGetInspectorDataForInstanceInProduction = false; export const enableInfiniteRenderLoopDetection = true; export const enableLazyContextPropagation = false; +export const enableContextProfiling = false; export const enableLegacyCache = false; export const enableLegacyFBSupport = false; export const enableLegacyHidden = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index 1364d2b81abb0..ee14bf6394580 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -55,6 +55,7 @@ export const transitionLaneExpirationMs = 5000; export const disableSchedulerTimeoutInWorkLoop = false; export const enableLazyContextPropagation = false; +export const enableContextProfiling = false; export const enableLegacyHidden = false; export const forceConcurrentByDefaultForTesting = false; export const allowConcurrentByDefault = true; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index ef118515c0cff..7a883aa5fbf14 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -78,6 +78,8 @@ export const enableTaint = false; export const enablePostpone = false; +export const enableContextProfiling = true; + // TODO: www currently relies on this feature. It's disabled in open source. // Need to remove it. export const disableCommentsAsDOMContainers = false; From b0f56925fb4f67274f584aafa8a53eea6648eeaa Mon Sep 17 00:00:00 2001 From: Jack Pope Date: Tue, 23 Jul 2024 12:45:03 -0400 Subject: [PATCH 2/6] Fork readContextForConsumer --- .../react-debug-tools/src/ReactDebugHooks.js | 6 +- .../react-reconciler/src/ReactFiberHooks.js | 16 +-- .../src/ReactFiberNewContext.js | 99 ++++++++++++++----- .../src/ReactInternalTypes.js | 26 +++-- packages/react/src/ReactHooks.js | 2 +- 5 files changed, 110 insertions(+), 39 deletions(-) diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js index 1ecc02aa3e652..f7815a36f2893 100644 --- a/packages/react-debug-tools/src/ReactDebugHooks.js +++ b/packages/react-debug-tools/src/ReactDebugHooks.js @@ -37,6 +37,7 @@ import { REACT_CONTEXT_TYPE, } from 'shared/ReactSymbols'; import hasOwnProperty from 'shared/hasOwnProperty'; +import type {ContextDependencyWithCompare} from '../../react-reconciler/src/ReactInternalTypes'; type CurrentDispatcherRef = typeof ReactSharedInternals; @@ -155,7 +156,10 @@ function getPrimitiveStackCache(): Map> { let currentFiber: null | Fiber = null; let currentHook: null | Hook = null; -let currentContextDependency: null | ContextDependency = null; +let currentContextDependency: + | null + | ContextDependency + | ContextDependencyWithCompare = null; function nextHook(): null | Hook { const hook = currentHook; diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index a2567bd64118e..27c91bf84c41a 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -1060,7 +1060,7 @@ function updateWorkInProgressHook(): Hook { function unstable_useContextWithBailout( context: ReactContext, - compare: void | (T => mixed), + compare: (T => mixed) | null, ): T { return readContextAndCompare(context, compare); } @@ -4049,7 +4049,7 @@ if (__DEV__) { } if (enableContextProfiling) { (HooksDispatcherOnMountInDEV: Dispatcher).unstable_useContextWithBailout = - function (context: ReactContext, compare: void | (T => mixed)): T { + function (context: ReactContext, compare: (T => mixed) | null): T { currentHookNameInDev = 'useContext'; mountHookTypesDev(); return unstable_useContextWithBailout(context, compare); @@ -4238,7 +4238,7 @@ if (__DEV__) { } if (enableContextProfiling) { (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).unstable_useContextWithBailout = - function (context: ReactContext, compare: void | (T => mixed)): T { + function (context: ReactContext, compare: (T => mixed) | null): T { currentHookNameInDev = 'useContext'; updateHookTypesDev(); return unstable_useContextWithBailout(context, compare); @@ -4426,7 +4426,7 @@ if (__DEV__) { } if (enableContextProfiling) { (HooksDispatcherOnUpdateInDEV: Dispatcher).unstable_useContextWithBailout = - function (context: ReactContext, compare: void | (T => mixed)): T { + function (context: ReactContext, compare: (T => mixed) | null): T { currentHookNameInDev = 'useContext'; updateHookTypesDev(); return unstable_useContextWithBailout(context, compare); @@ -4614,7 +4614,7 @@ if (__DEV__) { } if (enableContextProfiling) { (HooksDispatcherOnUpdateInDEV: Dispatcher).unstable_useContextWithBailout = - function (context: ReactContext, compare: void | (T => mixed)): T { + function (context: ReactContext, compare: (T => mixed) | null): T { currentHookNameInDev = 'useContext'; updateHookTypesDev(); return unstable_useContextWithBailout(context, compare); @@ -4828,7 +4828,7 @@ if (__DEV__) { } if (enableContextProfiling) { (HooksDispatcherOnUpdateInDEV: Dispatcher).unstable_useContextWithBailout = - function (context: ReactContext, compare: void | (T => mixed)): T { + function (context: ReactContext, compare: (T => mixed) | null): T { currentHookNameInDev = 'useContext'; warnInvalidHookAccess(); mountHookTypesDev(); @@ -5043,7 +5043,7 @@ if (__DEV__) { } if (enableContextProfiling) { (InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).unstable_useContextWithBailout = - function (context: ReactContext, compare: void | (T => mixed)): T { + function (context: ReactContext, compare: (T => mixed) | null): T { currentHookNameInDev = 'useContext'; warnInvalidHookAccess(); updateHookTypesDev(); @@ -5258,7 +5258,7 @@ if (__DEV__) { } if (enableContextProfiling) { (InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).unstable_useContextWithBailout = - function (context: ReactContext, compare: void | (T => mixed)): T { + function (context: ReactContext, compare: (T => mixed) | null): T { currentHookNameInDev = 'useContext'; warnInvalidHookAccess(); updateHookTypesDev(); diff --git a/packages/react-reconciler/src/ReactFiberNewContext.js b/packages/react-reconciler/src/ReactFiberNewContext.js index bec21a08686fe..d5a38fdb43bb8 100644 --- a/packages/react-reconciler/src/ReactFiberNewContext.js +++ b/packages/react-reconciler/src/ReactFiberNewContext.js @@ -12,6 +12,7 @@ import type { Fiber, ContextDependency, Dependencies, + ContextDependencyWithCompare, } from './ReactInternalTypes'; import type {StackCursor} from './ReactFiberStack'; import type {Lanes} from './ReactFiberLane'; @@ -72,7 +73,10 @@ if (__DEV__) { } let currentlyRenderingFiber: Fiber | null = null; -let lastContextDependency: ContextDependency | null = null; +let lastContextDependency: + | ContextDependency + | ContextDependencyWithCompare + | null = null; let lastFullyObservedContext: ReactContext | null = null; let isDisallowedContextReadInDEV: boolean = false; @@ -403,19 +407,21 @@ function propagateContextChanges( const context: ReactContext = contexts[i]; // Check if the context matches. if (dependency.context === context) { - const compare = dependency.compare; - if (enableContextProfiling && compare != null) { - const newValue = isPrimaryRenderer - ? dependency.context._currentValue - : dependency.context._currentValue2; - if ( - !checkIfComparedContextValuesChanged( - dependency.lastComparedValue, - compare(newValue), - ) - ) { - // Compared value hasn't changed. Bail out early. - continue findContext; + if (enableContextProfiling) { + const compare = dependency.compare; + if (compare != null) { + const newValue = isPrimaryRenderer + ? dependency.context._currentValue + : dependency.context._currentValue2; + if ( + !checkIfComparedContextValuesChanged( + dependency.lastComparedValue, + compare(newValue), + ) + ) { + // Compared value hasn't changed. Bail out early. + continue findContext; + } } } // Match! Schedule an update on this fiber. @@ -697,12 +703,11 @@ export function checkIfContextChanged( ? context._currentValue : context._currentValue2; const oldValue = dependency.memoizedValue; - const compare = dependency.compare; - if (enableContextProfiling && compare != null) { + if (enableContextProfiling && dependency.compare != null) { if ( checkIfComparedContextValuesChanged( dependency.lastComparedValue, - compare(newValue), + dependency.compare(newValue), ) ) { return true; @@ -746,13 +751,17 @@ export function prepareToReadContext( export function readContextAndCompare( context: ReactContext, - compare: void | (C => mixed), + compare: (C => mixed) | null, ): C { if (!enableLazyContextPropagation) { return readContext(context); } - return readContextForConsumer(currentlyRenderingFiber, context, compare); + return readContextForConsumer_withCompare( + currentlyRenderingFiber, + context, + compare, + ); } export function readContext(context: ReactContext): T { @@ -782,12 +791,12 @@ export function readContextDuringReconciliation( return readContextForConsumer(consumer, context); } -type ContextCompare = C => S; +type ContextCompare = C => V | null; -function readContextForConsumer( +function readContextForConsumer_withCompare( consumer: Fiber | null, context: ReactContext, - compare?: void | (C => S), + compare: (C => S) | null, ): C { const value = isPrimaryRenderer ? context._currentValue @@ -800,7 +809,7 @@ function readContextForConsumer( context: ((context: any): ReactContext), memoizedValue: value, next: null, - compare: ((compare: any): ContextCompare | null), + compare: compare ? ((compare: any): ContextCompare) : null, lastComparedValue: compare != null ? compare(value) : null, }; @@ -830,3 +839,47 @@ function readContextForConsumer( } return value; } + +function readContextForConsumer( + consumer: Fiber | null, + context: ReactContext, +): C { + const value = isPrimaryRenderer + ? context._currentValue + : context._currentValue2; + + if (lastFullyObservedContext === context) { + // Nothing to do. We already observe everything in this context. + } else { + const contextItem = { + context: ((context: any): ReactContext), + memoizedValue: value, + next: null, + }; + + if (lastContextDependency === null) { + if (consumer === null) { + throw new Error( + 'Context can only be read while React is rendering. ' + + 'In classes, you can read it in the render method or getDerivedStateFromProps. ' + + 'In function components, you can read it directly in the function body, but not ' + + 'inside Hooks like useReducer() or useMemo().', + ); + } + + // This is the first dependency for this component. Create a new list. + lastContextDependency = contextItem; + consumer.dependencies = { + lanes: NoLanes, + firstContext: contextItem, + }; + if (enableLazyContextPropagation) { + consumer.flags |= NeedsPropagation; + } + } else { + // Append a new context item. + lastContextDependency = lastContextDependency.next = contextItem; + } + } + return value; +} diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js index 67c260d78a0e9..1e033e8395a81 100644 --- a/packages/react-reconciler/src/ReactInternalTypes.js +++ b/packages/react-reconciler/src/ReactInternalTypes.js @@ -61,18 +61,32 @@ export type HookType = | 'useFormState' | 'useActionState'; -export type ContextDependency = { +export type ContextDependency = { context: ReactContext, - next: ContextDependency | null, + next: + | ContextDependency + | ContextDependencyWithCompare + | null, + memoizedValue: C, +}; + +export type ContextDependencyWithCompare = { + context: ReactContext, + next: + | ContextDependency + | ContextDependencyWithCompare + | null, memoizedValue: C, compare: (C => S) | null, - lastComparedValue: S | null, - ... + lastComparedValue?: S | null, }; export type Dependencies = { lanes: Lanes, - firstContext: ContextDependency | null, + firstContext: + | ContextDependency + | ContextDependencyWithCompare + | null, ... }; @@ -388,7 +402,7 @@ export type Dispatcher = { ): [S, Dispatch], unstable_useContextWithBailout?: ( context: ReactContext, - compare: void | (T => mixed), + compare: (T => mixed) | null, ) => T, useContext(context: ReactContext): T, useRef(initialValue: T): {current: T}, diff --git a/packages/react/src/ReactHooks.js b/packages/react/src/ReactHooks.js index 697752ef9698e..f9e243c397519 100644 --- a/packages/react/src/ReactHooks.js +++ b/packages/react/src/ReactHooks.js @@ -71,7 +71,7 @@ export function useContext(Context: ReactContext): T { export function unstable_useContextWithBailout( context: ReactContext, - compare: void | (T => mixed), + compare: (T => mixed) | null, ): T { if (!(enableLazyContextPropagation && enableContextProfiling)) { throw new Error('Not implemented.'); From f106363b6979de12a9661d3b0a588abcbe32909a Mon Sep 17 00:00:00 2001 From: Jack Pope Date: Wed, 24 Jul 2024 14:27:54 -0400 Subject: [PATCH 3/6] Check length of compared arrays --- packages/react-reconciler/src/ReactFiberNewContext.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberNewContext.js b/packages/react-reconciler/src/ReactFiberNewContext.js index d5a38fdb43bb8..76a8b0f1ade32 100644 --- a/packages/react-reconciler/src/ReactFiberNewContext.js +++ b/packages/react-reconciler/src/ReactFiberNewContext.js @@ -668,11 +668,11 @@ function checkIfComparedContextValuesChanged( newComparedValue: mixed, ): boolean { if (isArray(oldComparedValue) && isArray(newComparedValue)) { - for ( - let i = 0; - i < oldComparedValue.length && i < newComparedValue.length; - i++ - ) { + if (oldComparedValue.length !== newComparedValue.length) { + return true; + } + + for (let i = 0; i < oldComparedValue.length; i++) { if (!is(newComparedValue[i], oldComparedValue[i])) { return true; } From 0f64b021230d550fef913e01494a61071e959b20 Mon Sep 17 00:00:00 2001 From: Jack Pope Date: Fri, 26 Jul 2024 11:48:02 -0400 Subject: [PATCH 4/6] Require array return --- .../react-reconciler/src/ReactFiberHooks.js | 40 +++++++++++++++---- .../src/ReactFiberNewContext.js | 33 ++++++++------- .../src/ReactInternalTypes.js | 6 +-- .../__tests__/ReactContextWithBailout-test.js | 23 +++++++---- packages/react/src/ReactHooks.js | 2 +- 5 files changed, 69 insertions(+), 35 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index 27c91bf84c41a..8b300ff616207 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -1060,8 +1060,11 @@ function updateWorkInProgressHook(): Hook { function unstable_useContextWithBailout( context: ReactContext, - compare: (T => mixed) | null, + compare: (T => Array) | null, ): T { + if (compare === null) { + return readContext(context); + } return readContextAndCompare(context, compare); } @@ -4049,7 +4052,10 @@ if (__DEV__) { } if (enableContextProfiling) { (HooksDispatcherOnMountInDEV: Dispatcher).unstable_useContextWithBailout = - function (context: ReactContext, compare: (T => mixed) | null): T { + function ( + context: ReactContext, + compare: (T => Array) | null, + ): T { currentHookNameInDev = 'useContext'; mountHookTypesDev(); return unstable_useContextWithBailout(context, compare); @@ -4238,7 +4244,10 @@ if (__DEV__) { } if (enableContextProfiling) { (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).unstable_useContextWithBailout = - function (context: ReactContext, compare: (T => mixed) | null): T { + function ( + context: ReactContext, + compare: (T => Array) | null, + ): T { currentHookNameInDev = 'useContext'; updateHookTypesDev(); return unstable_useContextWithBailout(context, compare); @@ -4426,7 +4435,10 @@ if (__DEV__) { } if (enableContextProfiling) { (HooksDispatcherOnUpdateInDEV: Dispatcher).unstable_useContextWithBailout = - function (context: ReactContext, compare: (T => mixed) | null): T { + function ( + context: ReactContext, + compare: (T => Array) | null, + ): T { currentHookNameInDev = 'useContext'; updateHookTypesDev(); return unstable_useContextWithBailout(context, compare); @@ -4614,7 +4626,10 @@ if (__DEV__) { } if (enableContextProfiling) { (HooksDispatcherOnUpdateInDEV: Dispatcher).unstable_useContextWithBailout = - function (context: ReactContext, compare: (T => mixed) | null): T { + function ( + context: ReactContext, + compare: (T => Array) | null, + ): T { currentHookNameInDev = 'useContext'; updateHookTypesDev(); return unstable_useContextWithBailout(context, compare); @@ -4828,7 +4843,10 @@ if (__DEV__) { } if (enableContextProfiling) { (HooksDispatcherOnUpdateInDEV: Dispatcher).unstable_useContextWithBailout = - function (context: ReactContext, compare: (T => mixed) | null): T { + function ( + context: ReactContext, + compare: (T => Array) | null, + ): T { currentHookNameInDev = 'useContext'; warnInvalidHookAccess(); mountHookTypesDev(); @@ -5043,7 +5061,10 @@ if (__DEV__) { } if (enableContextProfiling) { (InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).unstable_useContextWithBailout = - function (context: ReactContext, compare: (T => mixed) | null): T { + function ( + context: ReactContext, + compare: (T => Array) | null, + ): T { currentHookNameInDev = 'useContext'; warnInvalidHookAccess(); updateHookTypesDev(); @@ -5258,7 +5279,10 @@ if (__DEV__) { } if (enableContextProfiling) { (InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).unstable_useContextWithBailout = - function (context: ReactContext, compare: (T => mixed) | null): T { + function ( + context: ReactContext, + compare: (T => Array) | null, + ): T { currentHookNameInDev = 'useContext'; warnInvalidHookAccess(); updateHookTypesDev(); diff --git a/packages/react-reconciler/src/ReactFiberNewContext.js b/packages/react-reconciler/src/ReactFiberNewContext.js index 76a8b0f1ade32..43163be3b3dcd 100644 --- a/packages/react-reconciler/src/ReactFiberNewContext.js +++ b/packages/react-reconciler/src/ReactFiberNewContext.js @@ -409,7 +409,7 @@ function propagateContextChanges( if (dependency.context === context) { if (enableContextProfiling) { const compare = dependency.compare; - if (compare != null) { + if (compare != null && dependency.lastComparedValue != null) { const newValue = isPrimaryRenderer ? dependency.context._currentValue : dependency.context._currentValue2; @@ -664,9 +664,12 @@ function propagateParentContextChanges( } function checkIfComparedContextValuesChanged( - oldComparedValue: mixed, - newComparedValue: mixed, + oldComparedValue: Array, + newComparedValue: Array, ): boolean { + // We have an implicit contract that compare functions must return arrays. + // This allows us to compare multiple values in the same context access + // since compiling to additional hook calls regresses perf. if (isArray(oldComparedValue) && isArray(newComparedValue)) { if (oldComparedValue.length !== newComparedValue.length) { return true; @@ -678,9 +681,7 @@ function checkIfComparedContextValuesChanged( } } } else { - if (!is(newComparedValue, oldComparedValue)) { - return true; - } + throw new Error('Compared context values must be arrays'); } return false; } @@ -703,7 +704,11 @@ export function checkIfContextChanged( ? context._currentValue : context._currentValue2; const oldValue = dependency.memoizedValue; - if (enableContextProfiling && dependency.compare != null) { + if ( + enableContextProfiling && + dependency.compare != null && + dependency.lastComparedValue != null + ) { if ( checkIfComparedContextValuesChanged( dependency.lastComparedValue, @@ -751,10 +756,10 @@ export function prepareToReadContext( export function readContextAndCompare( context: ReactContext, - compare: (C => mixed) | null, + compare: C => Array, ): C { - if (!enableLazyContextPropagation) { - return readContext(context); + if (!(enableLazyContextPropagation && enableContextProfiling)) { + throw new Error('Not implemented.'); } return readContextForConsumer_withCompare( @@ -791,12 +796,10 @@ export function readContextDuringReconciliation( return readContextForConsumer(consumer, context); } -type ContextCompare = C => V | null; - function readContextForConsumer_withCompare( consumer: Fiber | null, context: ReactContext, - compare: (C => S) | null, + compare: C => Array, ): C { const value = isPrimaryRenderer ? context._currentValue @@ -809,8 +812,8 @@ function readContextForConsumer_withCompare( context: ((context: any): ReactContext), memoizedValue: value, next: null, - compare: compare ? ((compare: any): ContextCompare) : null, - lastComparedValue: compare != null ? compare(value) : null, + compare: ((compare: any): (context: mixed) => Array), + lastComparedValue: compare(value), }; if (lastContextDependency === null) { diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js index 1e033e8395a81..689f4fb4c7ed6 100644 --- a/packages/react-reconciler/src/ReactInternalTypes.js +++ b/packages/react-reconciler/src/ReactInternalTypes.js @@ -77,8 +77,8 @@ export type ContextDependencyWithCompare = { | ContextDependencyWithCompare | null, memoizedValue: C, - compare: (C => S) | null, - lastComparedValue?: S | null, + compare: C => Array, + lastComparedValue: ?Array, }; export type Dependencies = { @@ -402,7 +402,7 @@ export type Dispatcher = { ): [S, Dispatch], unstable_useContextWithBailout?: ( context: ReactContext, - compare: (T => mixed) | null, + compare: (T => Array) | null, ) => T, useContext(context: ReactContext): T, useRef(initialValue: T): {current: T}, diff --git a/packages/react-reconciler/src/__tests__/ReactContextWithBailout-test.js b/packages/react-reconciler/src/__tests__/ReactContextWithBailout-test.js index 5150e9240d3fe..c510206148b16 100644 --- a/packages/react-reconciler/src/__tests__/ReactContextWithBailout-test.js +++ b/packages/react-reconciler/src/__tests__/ReactContextWithBailout-test.js @@ -53,17 +53,23 @@ describe('ReactContextWithBailout', () => { }); function A() { - const {a} = unstable_useContextWithBailout(Context, context => context.a); + const {a} = unstable_useContextWithBailout(Context, context => [ + context.a, + ]); return ; } function B() { - const {b} = unstable_useContextWithBailout(Context, context => context.b); + const {b} = unstable_useContextWithBailout(Context, context => [ + context.b, + ]); return ; } function C() { - const {c} = unstable_useContextWithBailout(Context, context => context.c); + const {c} = unstable_useContextWithBailout(Context, context => [ + context.c, + ]); return ; } @@ -126,7 +132,9 @@ describe('ReactContextWithBailout', () => { }); function Child() { - const {a} = unstable_useContextWithBailout(Context, context => context.a); + const {a} = unstable_useContextWithBailout(Context, context => [ + context.a, + ]); const context = useContext(Context); return ; } @@ -175,10 +183,9 @@ describe('ReactContextWithBailout', () => { }); function Child() { - const {a} = unstable_useContextWithBailout( - ContextA, - context => context.a, - ); + const {a} = unstable_useContextWithBailout(ContextA, context => [ + context.a, + ]); const b = useContext(ContextB); return ; } diff --git a/packages/react/src/ReactHooks.js b/packages/react/src/ReactHooks.js index f9e243c397519..22b398652338d 100644 --- a/packages/react/src/ReactHooks.js +++ b/packages/react/src/ReactHooks.js @@ -71,7 +71,7 @@ export function useContext(Context: ReactContext): T { export function unstable_useContextWithBailout( context: ReactContext, - compare: (T => mixed) | null, + compare: (T => Array) | null, ): T { if (!(enableLazyContextPropagation && enableContextProfiling)) { throw new Error('Not implemented.'); From d278b8f9cabd4a5ec5d279b67e9971404dc6a1fe Mon Sep 17 00:00:00 2001 From: Jack Pope Date: Fri, 26 Jul 2024 11:54:46 -0400 Subject: [PATCH 5/6] Rename compare->select --- .../react-debug-tools/src/ReactDebugHooks.js | 4 +-- .../src/ReactFiberNewContext.js | 36 +++++++++---------- .../src/ReactInternalTypes.js | 20 ++++------- scripts/error-codes/codes.json | 5 +-- 4 files changed, 30 insertions(+), 35 deletions(-) diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js index f7815a36f2893..edbef05e259d1 100644 --- a/packages/react-debug-tools/src/ReactDebugHooks.js +++ b/packages/react-debug-tools/src/ReactDebugHooks.js @@ -37,7 +37,7 @@ import { REACT_CONTEXT_TYPE, } from 'shared/ReactSymbols'; import hasOwnProperty from 'shared/hasOwnProperty'; -import type {ContextDependencyWithCompare} from '../../react-reconciler/src/ReactInternalTypes'; +import type {ContextDependencyWithSelect} from '../../react-reconciler/src/ReactInternalTypes'; type CurrentDispatcherRef = typeof ReactSharedInternals; @@ -159,7 +159,7 @@ let currentHook: null | Hook = null; let currentContextDependency: | null | ContextDependency - | ContextDependencyWithCompare = null; + | ContextDependencyWithSelect = null; function nextHook(): null | Hook { const hook = currentHook; diff --git a/packages/react-reconciler/src/ReactFiberNewContext.js b/packages/react-reconciler/src/ReactFiberNewContext.js index 43163be3b3dcd..497f6c39c0fce 100644 --- a/packages/react-reconciler/src/ReactFiberNewContext.js +++ b/packages/react-reconciler/src/ReactFiberNewContext.js @@ -12,7 +12,7 @@ import type { Fiber, ContextDependency, Dependencies, - ContextDependencyWithCompare, + ContextDependencyWithSelect, } from './ReactInternalTypes'; import type {StackCursor} from './ReactFiberStack'; import type {Lanes} from './ReactFiberLane'; @@ -75,7 +75,7 @@ if (__DEV__) { let currentlyRenderingFiber: Fiber | null = null; let lastContextDependency: | ContextDependency - | ContextDependencyWithCompare + | ContextDependencyWithSelect | null = null; let lastFullyObservedContext: ReactContext | null = null; @@ -408,15 +408,15 @@ function propagateContextChanges( // Check if the context matches. if (dependency.context === context) { if (enableContextProfiling) { - const compare = dependency.compare; - if (compare != null && dependency.lastComparedValue != null) { + const select = dependency.select; + if (select != null && dependency.lastSelectedValue != null) { const newValue = isPrimaryRenderer ? dependency.context._currentValue : dependency.context._currentValue2; if ( - !checkIfComparedContextValuesChanged( - dependency.lastComparedValue, - compare(newValue), + !checkIfSelectedContextValuesChanged( + dependency.lastSelectedValue, + select(newValue), ) ) { // Compared value hasn't changed. Bail out early. @@ -663,7 +663,7 @@ function propagateParentContextChanges( workInProgress.flags |= DidPropagateContext; } -function checkIfComparedContextValuesChanged( +function checkIfSelectedContextValuesChanged( oldComparedValue: Array, newComparedValue: Array, ): boolean { @@ -706,13 +706,13 @@ export function checkIfContextChanged( const oldValue = dependency.memoizedValue; if ( enableContextProfiling && - dependency.compare != null && - dependency.lastComparedValue != null + dependency.select != null && + dependency.lastSelectedValue != null ) { if ( - checkIfComparedContextValuesChanged( - dependency.lastComparedValue, - dependency.compare(newValue), + checkIfSelectedContextValuesChanged( + dependency.lastSelectedValue, + dependency.select(newValue), ) ) { return true; @@ -762,7 +762,7 @@ export function readContextAndCompare( throw new Error('Not implemented.'); } - return readContextForConsumer_withCompare( + return readContextForConsumer_withSelect( currentlyRenderingFiber, context, compare, @@ -796,10 +796,10 @@ export function readContextDuringReconciliation( return readContextForConsumer(consumer, context); } -function readContextForConsumer_withCompare( +function readContextForConsumer_withSelect( consumer: Fiber | null, context: ReactContext, - compare: C => Array, + select: C => Array, ): C { const value = isPrimaryRenderer ? context._currentValue @@ -812,8 +812,8 @@ function readContextForConsumer_withCompare( context: ((context: any): ReactContext), memoizedValue: value, next: null, - compare: ((compare: any): (context: mixed) => Array), - lastComparedValue: compare(value), + select: ((select: any): (context: mixed) => Array), + lastSelectedValue: select(value), }; if (lastContextDependency === null) { diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js index 689f4fb4c7ed6..4549253ba79b6 100644 --- a/packages/react-reconciler/src/ReactInternalTypes.js +++ b/packages/react-reconciler/src/ReactInternalTypes.js @@ -63,29 +63,23 @@ export type HookType = export type ContextDependency = { context: ReactContext, - next: - | ContextDependency - | ContextDependencyWithCompare - | null, + next: ContextDependency | ContextDependencyWithSelect | null, memoizedValue: C, }; -export type ContextDependencyWithCompare = { +export type ContextDependencyWithSelect = { context: ReactContext, - next: - | ContextDependency - | ContextDependencyWithCompare - | null, + next: ContextDependency | ContextDependencyWithSelect | null, memoizedValue: C, - compare: C => Array, - lastComparedValue: ?Array, + select: C => Array, + lastSelectedValue: ?Array, }; export type Dependencies = { lanes: Lanes, firstContext: | ContextDependency - | ContextDependencyWithCompare + | ContextDependencyWithSelect | null, ... }; @@ -402,7 +396,7 @@ export type Dispatcher = { ): [S, Dispatch], unstable_useContextWithBailout?: ( context: ReactContext, - compare: (T => Array) | null, + select: (T => Array) | null, ) => T, useContext(context: ReactContext): T, useRef(initialValue: T): {current: T}, diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index ff87311764f6c..ba4fb4fa28428 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -525,5 +525,6 @@ "537": "Cannot pass event handlers (%s) in renderToMarkup because the HTML will never be hydrated so they can never get called.", "538": "Cannot use state or effect Hooks in renderToMarkup because this component will never be hydrated.", "539": "Binary RSC chunks cannot be encoded as strings. This is a bug in the wiring of the React streams.", - "540": "String chunks need to be passed in their original shape. Not split into smaller string chunks. This is a bug in the wiring of the React streams." -} + "540": "String chunks need to be passed in their original shape. Not split into smaller string chunks. This is a bug in the wiring of the React streams.", + "541": "Compared context values must be arrays" +} \ No newline at end of file From 96136abb2bbaad77160097815cf0e53c2423f8be Mon Sep 17 00:00:00 2001 From: Jack Pope Date: Fri, 26 Jul 2024 13:30:06 -0400 Subject: [PATCH 6/6] f --- .../react-reconciler/src/ReactFiberHooks.js | 34 +++++++++---------- .../src/ReactFiberNewContext.js | 4 +-- packages/react/src/ReactHooks.js | 4 +-- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index 8b300ff616207..78c9c8a9e05ff 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -1060,12 +1060,12 @@ function updateWorkInProgressHook(): Hook { function unstable_useContextWithBailout( context: ReactContext, - compare: (T => Array) | null, + select: (T => Array) | null, ): T { - if (compare === null) { + if (select === null) { return readContext(context); } - return readContextAndCompare(context, compare); + return readContextAndCompare(context, select); } // NOTE: defining two versions of this function to avoid size impact when this feature is disabled. @@ -4054,11 +4054,11 @@ if (__DEV__) { (HooksDispatcherOnMountInDEV: Dispatcher).unstable_useContextWithBailout = function ( context: ReactContext, - compare: (T => Array) | null, + select: (T => Array) | null, ): T { currentHookNameInDev = 'useContext'; mountHookTypesDev(); - return unstable_useContextWithBailout(context, compare); + return unstable_useContextWithBailout(context, select); }; } @@ -4246,11 +4246,11 @@ if (__DEV__) { (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).unstable_useContextWithBailout = function ( context: ReactContext, - compare: (T => Array) | null, + select: (T => Array) | null, ): T { currentHookNameInDev = 'useContext'; updateHookTypesDev(); - return unstable_useContextWithBailout(context, compare); + return unstable_useContextWithBailout(context, select); }; } @@ -4437,11 +4437,11 @@ if (__DEV__) { (HooksDispatcherOnUpdateInDEV: Dispatcher).unstable_useContextWithBailout = function ( context: ReactContext, - compare: (T => Array) | null, + select: (T => Array) | null, ): T { currentHookNameInDev = 'useContext'; updateHookTypesDev(); - return unstable_useContextWithBailout(context, compare); + return unstable_useContextWithBailout(context, select); }; } @@ -4628,11 +4628,11 @@ if (__DEV__) { (HooksDispatcherOnUpdateInDEV: Dispatcher).unstable_useContextWithBailout = function ( context: ReactContext, - compare: (T => Array) | null, + select: (T => Array) | null, ): T { currentHookNameInDev = 'useContext'; updateHookTypesDev(); - return unstable_useContextWithBailout(context, compare); + return unstable_useContextWithBailout(context, select); }; } @@ -4845,12 +4845,12 @@ if (__DEV__) { (HooksDispatcherOnUpdateInDEV: Dispatcher).unstable_useContextWithBailout = function ( context: ReactContext, - compare: (T => Array) | null, + select: (T => Array) | null, ): T { currentHookNameInDev = 'useContext'; warnInvalidHookAccess(); mountHookTypesDev(); - return unstable_useContextWithBailout(context, compare); + return unstable_useContextWithBailout(context, select); }; } @@ -5063,12 +5063,12 @@ if (__DEV__) { (InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).unstable_useContextWithBailout = function ( context: ReactContext, - compare: (T => Array) | null, + select: (T => Array) | null, ): T { currentHookNameInDev = 'useContext'; warnInvalidHookAccess(); updateHookTypesDev(); - return unstable_useContextWithBailout(context, compare); + return unstable_useContextWithBailout(context, select); }; } @@ -5281,12 +5281,12 @@ if (__DEV__) { (InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).unstable_useContextWithBailout = function ( context: ReactContext, - compare: (T => Array) | null, + select: (T => Array) | null, ): T { currentHookNameInDev = 'useContext'; warnInvalidHookAccess(); updateHookTypesDev(); - return unstable_useContextWithBailout(context, compare); + return unstable_useContextWithBailout(context, select); }; } } diff --git a/packages/react-reconciler/src/ReactFiberNewContext.js b/packages/react-reconciler/src/ReactFiberNewContext.js index 497f6c39c0fce..cc908167e1e4f 100644 --- a/packages/react-reconciler/src/ReactFiberNewContext.js +++ b/packages/react-reconciler/src/ReactFiberNewContext.js @@ -756,7 +756,7 @@ export function prepareToReadContext( export function readContextAndCompare( context: ReactContext, - compare: C => Array, + select: C => Array, ): C { if (!(enableLazyContextPropagation && enableContextProfiling)) { throw new Error('Not implemented.'); @@ -765,7 +765,7 @@ export function readContextAndCompare( return readContextForConsumer_withSelect( currentlyRenderingFiber, context, - compare, + select, ); } diff --git a/packages/react/src/ReactHooks.js b/packages/react/src/ReactHooks.js index 22b398652338d..956a2a96b44a1 100644 --- a/packages/react/src/ReactHooks.js +++ b/packages/react/src/ReactHooks.js @@ -71,7 +71,7 @@ export function useContext(Context: ReactContext): T { export function unstable_useContextWithBailout( context: ReactContext, - compare: (T => Array) | null, + select: (T => Array) | null, ): T { if (!(enableLazyContextPropagation && enableContextProfiling)) { throw new Error('Not implemented.'); @@ -87,7 +87,7 @@ export function unstable_useContextWithBailout( } } // $FlowFixMe[not-a-function] This is unstable, thus optional - return dispatcher.unstable_useContextWithBailout(context, compare); + return dispatcher.unstable_useContextWithBailout(context, select); } export function useState(