diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js index ecfcc30d4bad0..612cba9d063d6 100644 --- a/packages/react-debug-tools/src/ReactDebugHooks.js +++ b/packages/react-debug-tools/src/ReactDebugHooks.js @@ -114,9 +114,9 @@ function readContext(context: ReactContext): T { return context._currentValue; } -function useContext( +function useContext( context: ReactContext, - options?: {unstable_selector?: T => S}, + options?: {unstable_selector?: T => mixed}, ): T { hookLog.push({ primitive: 'Context', diff --git a/packages/react-dom/src/server/ReactPartialRendererHooks.js b/packages/react-dom/src/server/ReactPartialRendererHooks.js index b692733f52fdf..a7219c58a8779 100644 --- a/packages/react-dom/src/server/ReactPartialRendererHooks.js +++ b/packages/react-dom/src/server/ReactPartialRendererHooks.js @@ -235,9 +235,9 @@ function readContext(context: ReactContext): T { return context[threadID]; } -function useContext( +function useContext( context: ReactContext, - options?: {unstable_selector?: T => S}, + options?: {unstable_selector?: T => mixed}, ): T { if (__DEV__) { currentHookNameInDev = 'useContext'; diff --git a/packages/react-reconciler/src/ReactFiberHooks.new.js b/packages/react-reconciler/src/ReactFiberHooks.new.js index bfabfb51f8d72..18d34bc6c3a8e 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.new.js +++ b/packages/react-reconciler/src/ReactFiberHooks.new.js @@ -680,9 +680,9 @@ function updateWorkInProgressHook(): Hook { return workInProgressHook; } -function useContext( +function useContext( context: ReactContext, - options?: {unstable_selector?: T => S}, + options?: {unstable_selector?: T => mixed}, ): T { if (options !== undefined) { const selector = options.unstable_selector; @@ -2216,9 +2216,9 @@ if (__DEV__) { checkDepsAreArrayDev(deps); return mountCallback(callback, deps); }, - useContext( + useContext( context: ReactContext, - options?: {unstable_selector?: T => S}, + options?: {unstable_selector?: T => mixed}, ): T { currentHookNameInDev = 'useContext'; mountHookTypesDev(); @@ -2347,9 +2347,9 @@ if (__DEV__) { updateHookTypesDev(); return mountCallback(callback, deps); }, - useContext( + useContext( context: ReactContext, - options?: {unstable_selector?: T => S}, + options?: {unstable_selector?: T => mixed}, ): T { currentHookNameInDev = 'useContext'; updateHookTypesDev(); @@ -2474,9 +2474,9 @@ if (__DEV__) { updateHookTypesDev(); return updateCallback(callback, deps); }, - useContext( + useContext( context: ReactContext, - options?: {unstable_selector?: T => S}, + options?: {unstable_selector?: T => mixed}, ): T { currentHookNameInDev = 'useContext'; updateHookTypesDev(); @@ -2602,9 +2602,9 @@ if (__DEV__) { updateHookTypesDev(); return updateCallback(callback, deps); }, - useContext( + useContext( context: ReactContext, - options?: {unstable_selector?: T => S}, + options?: {unstable_selector?: T => mixed}, ): T { currentHookNameInDev = 'useContext'; updateHookTypesDev(); @@ -2731,9 +2731,9 @@ if (__DEV__) { mountHookTypesDev(); return mountCallback(callback, deps); }, - useContext( + useContext( context: ReactContext, - options?: {unstable_selector?: T => S}, + options?: {unstable_selector?: T => mixed}, ): T { currentHookNameInDev = 'useContext'; warnInvalidHookAccess(); @@ -2873,9 +2873,9 @@ if (__DEV__) { updateHookTypesDev(); return updateCallback(callback, deps); }, - useContext( + useContext( context: ReactContext, - options?: {unstable_selector?: T => S}, + options?: {unstable_selector?: T => mixed}, ): T { currentHookNameInDev = 'useContext'; warnInvalidHookAccess(); @@ -3016,9 +3016,9 @@ if (__DEV__) { updateHookTypesDev(); return updateCallback(callback, deps); }, - useContext( + useContext( context: ReactContext, - options?: {unstable_selector?: T => S}, + options?: {unstable_selector?: T => mixed}, ): T { currentHookNameInDev = 'useContext'; warnInvalidHookAccess(); diff --git a/packages/react-reconciler/src/ReactFiberHooks.old.js b/packages/react-reconciler/src/ReactFiberHooks.old.js index 374ebb08299ac..d90e5f1fbf76a 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.old.js +++ b/packages/react-reconciler/src/ReactFiberHooks.old.js @@ -680,9 +680,9 @@ function updateWorkInProgressHook(): Hook { return workInProgressHook; } -function useContext( +function useContext( context: ReactContext, - options?: {unstable_selector?: T => S}, + options?: {unstable_selector?: T => mixed}, ): T { if (options !== undefined) { const selector = options.unstable_selector; @@ -2216,9 +2216,9 @@ if (__DEV__) { checkDepsAreArrayDev(deps); return mountCallback(callback, deps); }, - useContext( + useContext( context: ReactContext, - options?: {unstable_selector?: T => S}, + options?: {unstable_selector?: T => mixed}, ): T { currentHookNameInDev = 'useContext'; mountHookTypesDev(); @@ -2347,9 +2347,9 @@ if (__DEV__) { updateHookTypesDev(); return mountCallback(callback, deps); }, - useContext( + useContext( context: ReactContext, - options?: {unstable_selector?: T => S}, + options?: {unstable_selector?: T => mixed}, ): T { currentHookNameInDev = 'useContext'; updateHookTypesDev(); @@ -2474,9 +2474,9 @@ if (__DEV__) { updateHookTypesDev(); return updateCallback(callback, deps); }, - useContext( + useContext( context: ReactContext, - options?: {unstable_selector?: T => S}, + options?: {unstable_selector?: T => mixed}, ): T { currentHookNameInDev = 'useContext'; updateHookTypesDev(); @@ -2602,9 +2602,9 @@ if (__DEV__) { updateHookTypesDev(); return updateCallback(callback, deps); }, - useContext( + useContext( context: ReactContext, - options?: {unstable_selector?: T => S}, + options?: {unstable_selector?: T => mixed}, ): T { currentHookNameInDev = 'useContext'; updateHookTypesDev(); @@ -2731,9 +2731,9 @@ if (__DEV__) { mountHookTypesDev(); return mountCallback(callback, deps); }, - useContext( + useContext( context: ReactContext, - options?: {unstable_selector?: T => S}, + options?: {unstable_selector?: T => mixed}, ): T { currentHookNameInDev = 'useContext'; warnInvalidHookAccess(); @@ -2873,9 +2873,9 @@ if (__DEV__) { updateHookTypesDev(); return updateCallback(callback, deps); }, - useContext( + useContext( context: ReactContext, - options?: {unstable_selector?: T => S}, + options?: {unstable_selector?: T => mixed}, ): T { currentHookNameInDev = 'useContext'; warnInvalidHookAccess(); @@ -3016,9 +3016,9 @@ if (__DEV__) { updateHookTypesDev(); return updateCallback(callback, deps); }, - useContext( + useContext( context: ReactContext, - options?: {unstable_selector?: T => S}, + options?: {unstable_selector?: T => mixed}, ): T { currentHookNameInDev = 'useContext'; warnInvalidHookAccess(); diff --git a/packages/react-reconciler/src/ReactFiberNewContext.new.js b/packages/react-reconciler/src/ReactFiberNewContext.new.js index f208942b8fbf0..b5a564042ce7b 100644 --- a/packages/react-reconciler/src/ReactFiberNewContext.new.js +++ b/packages/react-reconciler/src/ReactFiberNewContext.new.js @@ -212,7 +212,6 @@ function propagateContextChange_eager( let dependency = list.firstContext; while (dependency !== null) { // Check if the context matches. - // TODO: Compare selected values to bail out early. if (dependency.context === context) { // Match! Schedule an update on this fiber. if (fiber.tag === ClassComponent) { @@ -348,8 +347,19 @@ 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 selector = dependency.selector; + if (selector !== null) { + const newValue = isPrimaryRenderer + ? context._currentValue + : context._currentValue2; + const newSelectedValue = selector(newValue); + const oldSelectedValue = dependency.selectedValue; + if (is(oldSelectedValue, selector(newSelectedValue))) { + // Selected value hasn't changed. Bail out early. + continue findContext; + } + } // Match! Schedule an update on this fiber. // In the lazy implemenation, don't mark a dirty flag on the @@ -571,10 +581,8 @@ export function checkIfContextChanged(currentDependencies: Dependencies) { const oldValue = dependency.memoizedValue; const selector = dependency.selector; if (selector !== null) { - // TODO: Alternatively, we could store the selected value on the context. - // However, we expect selectors to do nothing except access a subfield, - // so this is probably fine, too. - if (!is(selector(newValue), selector(oldValue))) { + const oldSelectedValue = dependency.selectedValue; + if (!is(selector(newValue), oldSelectedValue)) { return true; } } else { @@ -657,8 +665,11 @@ function readContextImpl( const contextItem = { context: ((context: any): ReactContext), selector: ((selector: any): ContextSelector | null), - // TODO: Store selected value so we can compare to that during propagation memoizedValue: value, + // TODO: If useContextSelector becomes a built-in API, then + // readContextWithSelector should return the selected value so that we + // don't call the selector twice. Will need to inline readContextImpl. + selectedValue: selector !== null ? selector(value) : null, next: null, }; diff --git a/packages/react-reconciler/src/ReactFiberNewContext.old.js b/packages/react-reconciler/src/ReactFiberNewContext.old.js index 6dfb0732fd2f7..da37ae511238d 100644 --- a/packages/react-reconciler/src/ReactFiberNewContext.old.js +++ b/packages/react-reconciler/src/ReactFiberNewContext.old.js @@ -212,7 +212,6 @@ function propagateContextChange_eager( let dependency = list.firstContext; while (dependency !== null) { // Check if the context matches. - // TODO: Compare selected values to bail out early. if (dependency.context === context) { // Match! Schedule an update on this fiber. if (fiber.tag === ClassComponent) { @@ -348,8 +347,19 @@ 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 selector = dependency.selector; + if (selector !== null) { + const newValue = isPrimaryRenderer + ? context._currentValue + : context._currentValue2; + const newSelectedValue = selector(newValue); + const oldSelectedValue = dependency.selectedValue; + if (is(oldSelectedValue, selector(newSelectedValue))) { + // Selected value hasn't changed. Bail out early. + continue findContext; + } + } // Match! Schedule an update on this fiber. // In the lazy implemenation, don't mark a dirty flag on the @@ -571,10 +581,8 @@ export function checkIfContextChanged(currentDependencies: Dependencies) { const oldValue = dependency.memoizedValue; const selector = dependency.selector; if (selector !== null) { - // TODO: Alternatively, we could store the selected value on the context. - // However, we expect selectors to do nothing except access a subfield, - // so this is probably fine, too. - if (!is(selector(newValue), selector(oldValue))) { + const oldSelectedValue = dependency.selectedValue; + if (!is(selector(newValue), oldSelectedValue)) { return true; } } else { @@ -657,8 +665,11 @@ function readContextImpl( const contextItem = { context: ((context: any): ReactContext), selector: ((selector: any): ContextSelector | null), - // TODO: Store selected value so we can compare to that during propagation memoizedValue: value, + // TODO: If useContextSelector becomes a built-in API, then + // readContextWithSelector should return the selected value so that we + // don't call the selector twice. Will need to inline readContextImpl. + selectedValue: selector !== null ? selector(value) : null, next: null, }; diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js index ea7a542942bf1..524703db00093 100644 --- a/packages/react-reconciler/src/ReactInternalTypes.js +++ b/packages/react-reconciler/src/ReactInternalTypes.js @@ -51,6 +51,7 @@ export type ContextDependency = { selector: (C => S) | null, next: ContextDependency | null, memoizedValue: C, + selectedValue: S | null, ... }; @@ -282,9 +283,9 @@ export type Dispatcher = {| initialArg: I, init?: (I) => S, ): [S, Dispatch], - useContext( + useContext( context: ReactContext, - options?: {unstable_selector?: T => S}, + options?: {unstable_selector?: T => mixed}, ): T, useRef(initialValue: T): {|current: T|}, useEffect( diff --git a/packages/react/src/ReactHooks.js b/packages/react/src/ReactHooks.js index 3518bade21f9d..5794c7d0d99e3 100644 --- a/packages/react/src/ReactHooks.js +++ b/packages/react/src/ReactHooks.js @@ -47,9 +47,9 @@ export function getCacheForType(resourceType: () => T): T { return dispatcher.getCacheForType(resourceType); } -export function useContext( +export function useContext( Context: ReactContext, - options?: {unstable_selector?: T => S}, + options?: {unstable_selector?: T => mixed}, ): T { const dispatcher = resolveDispatcher(); if (__DEV__) {