Skip to content

Commit eb46705

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 updated 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 6175785 commit eb46705

25 files changed

+784
-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 {
@@ -54,7 +55,7 @@ import {
5455
higherLanePriority,
5556
DefaultLanePriority,
5657
} from './ReactFiberLane.new';
57-
import {readContext} from './ReactFiberNewContext.new';
58+
import {readContext, readContextInsideHook} from './ReactFiberNewContext.new';
5859
import {HostRoot, CacheComponent} from './ReactWorkTags';
5960
import {
6061
Update as UpdateEffect,
@@ -634,6 +635,56 @@ function updateWorkInProgressHook(): Hook {
634635
return workInProgressHook;
635636
}
636637

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

20692120
useCallback: throwInvalidHookError,
20702121
useContext: throwInvalidHookError,
2122+
useSelectedContext: throwInvalidHookError,
20712123
useEffect: throwInvalidHookError,
20722124
useImperativeHandle: throwInvalidHookError,
20732125
useLayoutEffect: throwInvalidHookError,
@@ -2093,6 +2145,7 @@ const HooksDispatcherOnMount: Dispatcher = {
20932145

20942146
useCallback: mountCallback,
20952147
useContext: readContext,
2148+
useSelectedContext: mountSelectedContext,
20962149
useEffect: mountEffect,
20972150
useImperativeHandle: mountImperativeHandle,
20982151
useLayoutEffect: mountLayoutEffect,
@@ -2118,6 +2171,7 @@ const HooksDispatcherOnUpdate: Dispatcher = {
21182171

21192172
useCallback: updateCallback,
21202173
useContext: readContext,
2174+
useSelectedContext: updateSelectedContext,
21212175
useEffect: updateEffect,
21222176
useImperativeHandle: updateImperativeHandle,
21232177
useLayoutEffect: updateLayoutEffect,
@@ -2143,6 +2197,7 @@ const HooksDispatcherOnRerender: Dispatcher = {
21432197

21442198
useCallback: updateCallback,
21452199
useContext: readContext,
2200+
useSelectedContext: updateSelectedContext,
21462201
useEffect: updateEffect,
21472202
useImperativeHandle: updateImperativeHandle,
21482203
useLayoutEffect: updateLayoutEffect,
@@ -2211,6 +2266,21 @@ if (__DEV__) {
22112266
mountHookTypesDev();
22122267
return readContext(context, observedBits);
22132268
},
2269+
useSelectedContext<C, S>(
2270+
context: ReactContext<C>,
2271+
selector: C => S,
2272+
isEqual: ((S, S) => boolean) | void,
2273+
): S {
2274+
currentHookNameInDev = 'useSelectedContext';
2275+
mountHookTypesDev();
2276+
const prevDispatcher = ReactCurrentDispatcher.current;
2277+
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
2278+
try {
2279+
return mountSelectedContext(context, selector, isEqual);
2280+
} finally {
2281+
ReactCurrentDispatcher.current = prevDispatcher;
2282+
}
2283+
},
22142284
useEffect(
22152285
create: () => (() => void) | void,
22162286
deps: Array<mixed> | void | null,
@@ -2345,6 +2415,21 @@ if (__DEV__) {
23452415
updateHookTypesDev();
23462416
return readContext(context, observedBits);
23472417
},
2418+
useSelectedContext<C, S>(
2419+
context: ReactContext<C>,
2420+
selector: C => S,
2421+
isEqual: ((S, S) => boolean) | void,
2422+
): S {
2423+
currentHookNameInDev = 'useSelectedContext';
2424+
updateHookTypesDev();
2425+
const prevDispatcher = ReactCurrentDispatcher.current;
2426+
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
2427+
try {
2428+
return mountSelectedContext(context, selector, isEqual);
2429+
} finally {
2430+
ReactCurrentDispatcher.current = prevDispatcher;
2431+
}
2432+
},
23482433
useEffect(
23492434
create: () => (() => void) | void,
23502435
deps: Array<mixed> | void | null,
@@ -2475,6 +2560,21 @@ if (__DEV__) {
24752560
updateHookTypesDev();
24762561
return readContext(context, observedBits);
24772562
},
2563+
useSelectedContext<C, S>(
2564+
context: ReactContext<C>,
2565+
selector: C => S,
2566+
isEqual: ((S, S) => boolean) | void,
2567+
): S {
2568+
currentHookNameInDev = 'useSelectedContext';
2569+
updateHookTypesDev();
2570+
const prevDispatcher = ReactCurrentDispatcher.current;
2571+
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
2572+
try {
2573+
return updateSelectedContext(context, selector, isEqual);
2574+
} finally {
2575+
ReactCurrentDispatcher.current = prevDispatcher;
2576+
}
2577+
},
24782578
useEffect(
24792579
create: () => (() => void) | void,
24802580
deps: Array<mixed> | void | null,
@@ -2606,6 +2706,21 @@ if (__DEV__) {
26062706
updateHookTypesDev();
26072707
return readContext(context, observedBits);
26082708
},
2709+
useSelectedContext<C, S>(
2710+
context: ReactContext<C>,
2711+
selector: C => S,
2712+
isEqual: ((S, S) => boolean) | void,
2713+
): S {
2714+
currentHookNameInDev = 'useSelectedContext';
2715+
updateHookTypesDev();
2716+
const prevDispatcher = ReactCurrentDispatcher.current;
2717+
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnRerenderInDEV;
2718+
try {
2719+
return updateSelectedContext(context, selector, isEqual);
2720+
} finally {
2721+
ReactCurrentDispatcher.current = prevDispatcher;
2722+
}
2723+
},
26092724
useEffect(
26102725
create: () => (() => void) | void,
26112726
deps: Array<mixed> | void | null,
@@ -2739,6 +2854,22 @@ if (__DEV__) {
27392854
mountHookTypesDev();
27402855
return readContext(context, observedBits);
27412856
},
2857+
useSelectedContext<C, S>(
2858+
context: ReactContext<C>,
2859+
selector: C => S,
2860+
isEqual: ((S, S) => boolean) | void,
2861+
): S {
2862+
currentHookNameInDev = 'useSelectedContext';
2863+
warnInvalidHookAccess();
2864+
mountHookTypesDev();
2865+
const prevDispatcher = ReactCurrentDispatcher.current;
2866+
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
2867+
try {
2868+
return mountSelectedContext(context, selector, isEqual);
2869+
} finally {
2870+
ReactCurrentDispatcher.current = prevDispatcher;
2871+
}
2872+
},
27422873
useEffect(
27432874
create: () => (() => void) | void,
27442875
deps: Array<mixed> | void | null,
@@ -2884,6 +3015,22 @@ if (__DEV__) {
28843015
updateHookTypesDev();
28853016
return readContext(context, observedBits);
28863017
},
3018+
useSelectedContext<C, S>(
3019+
context: ReactContext<C>,
3020+
selector: C => S,
3021+
isEqual: ((S, S) => boolean) | void,
3022+
): S {
3023+
currentHookNameInDev = 'useSelectedContext';
3024+
warnInvalidHookAccess();
3025+
updateHookTypesDev();
3026+
const prevDispatcher = ReactCurrentDispatcher.current;
3027+
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
3028+
try {
3029+
return updateSelectedContext(context, selector, isEqual);
3030+
} finally {
3031+
ReactCurrentDispatcher.current = prevDispatcher;
3032+
}
3033+
},
28873034
useEffect(
28883035
create: () => (() => void) | void,
28893036
deps: Array<mixed> | void | null,
@@ -3030,6 +3177,22 @@ if (__DEV__) {
30303177
updateHookTypesDev();
30313178
return readContext(context, observedBits);
30323179
},
3180+
useSelectedContext<C, S>(
3181+
context: ReactContext<C>,
3182+
selector: C => S,
3183+
isEqual: ((S, S) => boolean) | void,
3184+
): S {
3185+
currentHookNameInDev = 'useSelectedContext';
3186+
warnInvalidHookAccess();
3187+
updateHookTypesDev();
3188+
const prevDispatcher = ReactCurrentDispatcher.current;
3189+
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
3190+
try {
3191+
return updateSelectedContext(context, selector, isEqual);
3192+
} finally {
3193+
ReactCurrentDispatcher.current = prevDispatcher;
3194+
}
3195+
},
30333196
useEffect(
30343197
create: () => (() => void) | void,
30353198
deps: Array<mixed> | void | null,

0 commit comments

Comments
 (0)