Skip to content

Commit aeace50

Browse files
committed
Implement naive version of context selectors
For internal experimentation only. This implements `unstable_useSelectedContext` behind a feature flag. It's based on [RFC 119](reactjs/rfcs#119) and [RFC 118](reactjs/rfcs#118) by @gnoff. Usage: ```js const selection = useSelectedContext(Context, c => select(c)); ``` The key feature is that if the selected value does not change between renders, the component will bail out of rendering its children, a la `memo`, `PureComponent`, or the `useState` bailout mechanism. (Unless some other state, props, or context was received in the same render.) However, I have not implemented the RFC's proposed optimizations to context propagation. We would like to land those eventually, but doing so will require a refactor that we don't currently have the bandwidth to complete. It will need to wait until after React 18. In the meantime though, we believe there may be value in landing this more naive implementation. It's designed to be API-compatible with the full proposal, so we have the option to make those optimizations in a non-breaking release. However, since it's still behind a flag, this currently has no impact on the stable release channel. We reserve the right to change or remove it, as we conduct internal testing. I also added an optional third argument, `isSelectionEqual`. If defined, it will override the default comparison function used to check if the selected value has changed (`Object.is`).
1 parent f15f8f6 commit aeace50

24 files changed

+783
-16
lines changed

packages/react-debug-tools/src/ReactDebugHooks.js

+16
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,21 @@ function useContext<T>(
129129
return context._currentValue;
130130
}
131131

132+
function useSelectedContext<C, S>(
133+
Context: ReactContext<C>,
134+
selector: C => S,
135+
isEqual: ((S, S) => boolean) | void,
136+
): S {
137+
const context = Context._currentValue;
138+
const selection = selector(context);
139+
hookLog.push({
140+
primitive: 'SelectedContext',
141+
stackError: new Error(),
142+
value: selection,
143+
});
144+
return selection;
145+
}
146+
132147
function useState<S>(
133148
initialState: (() => S) | S,
134149
): [S, Dispatch<BasicStateAction<S>>] {
@@ -322,6 +337,7 @@ const Dispatcher: DispatcherType = {
322337
useCacheRefresh,
323338
useCallback,
324339
useContext,
340+
useSelectedContext,
325341
useEffect,
326342
useImperativeHandle,
327343
useDebugValue,

packages/react-dom/src/server/ReactPartialRendererHooks.js

+17
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,22 @@ function useContext<T>(
251251
return context[threadID];
252252
}
253253

254+
function useSelectedContext<C, S>(
255+
Context: ReactContext<C>,
256+
selector: C => S,
257+
isEqual: ((S, S) => boolean) | void,
258+
): S {
259+
if (__DEV__) {
260+
currentHookNameInDev = 'useSelectedContext';
261+
}
262+
resolveCurrentlyRenderingComponent();
263+
const threadID = currentPartialRenderer.threadID;
264+
validateContextBounds(Context, threadID);
265+
const context = Context[threadID];
266+
const selection = selector(context);
267+
return selection;
268+
}
269+
254270
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
255271
// $FlowFixMe: Flow doesn't like mixed types
256272
return typeof action === 'function' ? action(state) : action;
@@ -503,6 +519,7 @@ export function setCurrentPartialRenderer(renderer: PartialRenderer) {
503519
export const Dispatcher: DispatcherType = {
504520
readContext,
505521
useContext,
522+
useSelectedContext,
506523
useMemo,
507524
useReducer,
508525
useRef,

packages/react-reconciler/src/ReactFiberHooks.new.js

+164-1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
decoupleUpdatePriorityFromScheduler,
3131
enableUseRefAccessWarning,
3232
enableDoubleInvokingEffects,
33+
enableContextSelectors,
3334
} from 'shared/ReactFeatureFlags';
3435

3536
import {
@@ -52,7 +53,7 @@ import {
5253
higherLanePriority,
5354
DefaultLanePriority,
5455
} from './ReactFiberLane.new';
55-
import {readContext} from './ReactFiberNewContext.new';
56+
import {readContext, readContextInsideHook} from './ReactFiberNewContext.new';
5657
import {HostRoot, CacheComponent} from './ReactWorkTags';
5758
import {
5859
Update as UpdateEffect,
@@ -627,6 +628,56 @@ function updateWorkInProgressHook(): Hook {
627628
return workInProgressHook;
628629
}
629630

631+
function mountSelectedContext<C, S>(
632+
Context: ReactContext<C>,
633+
selector: C => S,
634+
isEqual: ((S, S) => boolean) | void,
635+
): S {
636+
if (!enableContextSelectors) {
637+
return (undefined: any);
638+
}
639+
640+
const hook = mountWorkInProgressHook();
641+
const context = readContextInsideHook(Context);
642+
const selection = selector(context);
643+
hook.memoizedState = selection;
644+
return selection;
645+
}
646+
647+
function updateSelectedContext<C, S>(
648+
Context: ReactContext<C>,
649+
selector: C => S,
650+
isEqual: ((S, S) => boolean) | void,
651+
): S {
652+
if (!enableContextSelectors) {
653+
return (undefined: any);
654+
}
655+
656+
const hook = updateWorkInProgressHook();
657+
const context = readContextInsideHook(Context);
658+
const newSelection = selector(context);
659+
const oldSelection: S = hook.memoizedState;
660+
if (isEqual !== undefined) {
661+
if (__DEV__) {
662+
if (typeof isEqual !== 'function') {
663+
console.error(
664+
'The optional third argument to useSelectedContext must be a ' +
665+
'function. Instead got: %s',
666+
isEqual,
667+
);
668+
}
669+
}
670+
if (isEqual(newSelection, oldSelection)) {
671+
return oldSelection;
672+
}
673+
} else if (is(newSelection, oldSelection)) {
674+
return oldSelection;
675+
}
676+
markWorkInProgressReceivedUpdate();
677+
hook.memoizedState = newSelection;
678+
return newSelection;
679+
}
680+
630681
function createFunctionComponentUpdateQueue(): FunctionComponentUpdateQueue {
631682
return {
632683
lastEffect: null,
@@ -1995,6 +2046,7 @@ export const ContextOnlyDispatcher: Dispatcher = {
19952046

19962047
useCallback: throwInvalidHookError,
19972048
useContext: throwInvalidHookError,
2049+
useSelectedContext: throwInvalidHookError,
19982050
useEffect: throwInvalidHookError,
19992051
useImperativeHandle: throwInvalidHookError,
20002052
useLayoutEffect: throwInvalidHookError,
@@ -2020,6 +2072,7 @@ const HooksDispatcherOnMount: Dispatcher = {
20202072

20212073
useCallback: mountCallback,
20222074
useContext: readContext,
2075+
useSelectedContext: mountSelectedContext,
20232076
useEffect: mountEffect,
20242077
useImperativeHandle: mountImperativeHandle,
20252078
useLayoutEffect: mountLayoutEffect,
@@ -2045,6 +2098,7 @@ const HooksDispatcherOnUpdate: Dispatcher = {
20452098

20462099
useCallback: updateCallback,
20472100
useContext: readContext,
2101+
useSelectedContext: updateSelectedContext,
20482102
useEffect: updateEffect,
20492103
useImperativeHandle: updateImperativeHandle,
20502104
useLayoutEffect: updateLayoutEffect,
@@ -2070,6 +2124,7 @@ const HooksDispatcherOnRerender: Dispatcher = {
20702124

20712125
useCallback: updateCallback,
20722126
useContext: readContext,
2127+
useSelectedContext: updateSelectedContext,
20732128
useEffect: updateEffect,
20742129
useImperativeHandle: updateImperativeHandle,
20752130
useLayoutEffect: updateLayoutEffect,
@@ -2138,6 +2193,21 @@ if (__DEV__) {
21382193
mountHookTypesDev();
21392194
return readContext(context, observedBits);
21402195
},
2196+
useSelectedContext<C, S>(
2197+
context: ReactContext<C>,
2198+
selector: C => S,
2199+
isEqual: ((S, S) => boolean) | void,
2200+
): S {
2201+
currentHookNameInDev = 'useSelectedContext';
2202+
mountHookTypesDev();
2203+
const prevDispatcher = ReactCurrentDispatcher.current;
2204+
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
2205+
try {
2206+
return mountSelectedContext(context, selector, isEqual);
2207+
} finally {
2208+
ReactCurrentDispatcher.current = prevDispatcher;
2209+
}
2210+
},
21412211
useEffect(
21422212
create: () => (() => void) | void,
21432213
deps: Array<mixed> | void | null,
@@ -2272,6 +2342,21 @@ if (__DEV__) {
22722342
updateHookTypesDev();
22732343
return readContext(context, observedBits);
22742344
},
2345+
useSelectedContext<C, S>(
2346+
context: ReactContext<C>,
2347+
selector: C => S,
2348+
isEqual: ((S, S) => boolean) | void,
2349+
): S {
2350+
currentHookNameInDev = 'useSelectedContext';
2351+
updateHookTypesDev();
2352+
const prevDispatcher = ReactCurrentDispatcher.current;
2353+
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
2354+
try {
2355+
return mountSelectedContext(context, selector, isEqual);
2356+
} finally {
2357+
ReactCurrentDispatcher.current = prevDispatcher;
2358+
}
2359+
},
22752360
useEffect(
22762361
create: () => (() => void) | void,
22772362
deps: Array<mixed> | void | null,
@@ -2402,6 +2487,21 @@ if (__DEV__) {
24022487
updateHookTypesDev();
24032488
return readContext(context, observedBits);
24042489
},
2490+
useSelectedContext<C, S>(
2491+
context: ReactContext<C>,
2492+
selector: C => S,
2493+
isEqual: ((S, S) => boolean) | void,
2494+
): S {
2495+
currentHookNameInDev = 'useSelectedContext';
2496+
updateHookTypesDev();
2497+
const prevDispatcher = ReactCurrentDispatcher.current;
2498+
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
2499+
try {
2500+
return updateSelectedContext(context, selector, isEqual);
2501+
} finally {
2502+
ReactCurrentDispatcher.current = prevDispatcher;
2503+
}
2504+
},
24052505
useEffect(
24062506
create: () => (() => void) | void,
24072507
deps: Array<mixed> | void | null,
@@ -2533,6 +2633,21 @@ if (__DEV__) {
25332633
updateHookTypesDev();
25342634
return readContext(context, observedBits);
25352635
},
2636+
useSelectedContext<C, S>(
2637+
context: ReactContext<C>,
2638+
selector: C => S,
2639+
isEqual: ((S, S) => boolean) | void,
2640+
): S {
2641+
currentHookNameInDev = 'useSelectedContext';
2642+
updateHookTypesDev();
2643+
const prevDispatcher = ReactCurrentDispatcher.current;
2644+
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnRerenderInDEV;
2645+
try {
2646+
return updateSelectedContext(context, selector, isEqual);
2647+
} finally {
2648+
ReactCurrentDispatcher.current = prevDispatcher;
2649+
}
2650+
},
25362651
useEffect(
25372652
create: () => (() => void) | void,
25382653
deps: Array<mixed> | void | null,
@@ -2666,6 +2781,22 @@ if (__DEV__) {
26662781
mountHookTypesDev();
26672782
return readContext(context, observedBits);
26682783
},
2784+
useSelectedContext<C, S>(
2785+
context: ReactContext<C>,
2786+
selector: C => S,
2787+
isEqual: ((S, S) => boolean) | void,
2788+
): S {
2789+
currentHookNameInDev = 'useSelectedContext';
2790+
warnInvalidHookAccess();
2791+
mountHookTypesDev();
2792+
const prevDispatcher = ReactCurrentDispatcher.current;
2793+
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
2794+
try {
2795+
return mountSelectedContext(context, selector, isEqual);
2796+
} finally {
2797+
ReactCurrentDispatcher.current = prevDispatcher;
2798+
}
2799+
},
26692800
useEffect(
26702801
create: () => (() => void) | void,
26712802
deps: Array<mixed> | void | null,
@@ -2811,6 +2942,22 @@ if (__DEV__) {
28112942
updateHookTypesDev();
28122943
return readContext(context, observedBits);
28132944
},
2945+
useSelectedContext<C, S>(
2946+
context: ReactContext<C>,
2947+
selector: C => S,
2948+
isEqual: ((S, S) => boolean) | void,
2949+
): S {
2950+
currentHookNameInDev = 'useSelectedContext';
2951+
warnInvalidHookAccess();
2952+
updateHookTypesDev();
2953+
const prevDispatcher = ReactCurrentDispatcher.current;
2954+
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
2955+
try {
2956+
return updateSelectedContext(context, selector, isEqual);
2957+
} finally {
2958+
ReactCurrentDispatcher.current = prevDispatcher;
2959+
}
2960+
},
28142961
useEffect(
28152962
create: () => (() => void) | void,
28162963
deps: Array<mixed> | void | null,
@@ -2957,6 +3104,22 @@ if (__DEV__) {
29573104
updateHookTypesDev();
29583105
return readContext(context, observedBits);
29593106
},
3107+
useSelectedContext<C, S>(
3108+
context: ReactContext<C>,
3109+
selector: C => S,
3110+
isEqual: ((S, S) => boolean) | void,
3111+
): S {
3112+
currentHookNameInDev = 'useSelectedContext';
3113+
warnInvalidHookAccess();
3114+
updateHookTypesDev();
3115+
const prevDispatcher = ReactCurrentDispatcher.current;
3116+
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
3117+
try {
3118+
return updateSelectedContext(context, selector, isEqual);
3119+
} finally {
3120+
ReactCurrentDispatcher.current = prevDispatcher;
3121+
}
3122+
},
29603123
useEffect(
29613124
create: () => (() => void) | void,
29623125
deps: Array<mixed> | void | null,

0 commit comments

Comments
 (0)