Skip to content

Commit 730d3a0

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 47dd9f4 commit 730d3a0

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,
@@ -2069,6 +2120,7 @@ export const ContextOnlyDispatcher: Dispatcher = {
20692120

20702121
useCallback: throwInvalidHookError,
20712122
useContext: throwInvalidHookError,
2123+
useSelectedContext: throwInvalidHookError,
20722124
useEffect: throwInvalidHookError,
20732125
useImperativeHandle: throwInvalidHookError,
20742126
useLayoutEffect: throwInvalidHookError,
@@ -2094,6 +2146,7 @@ const HooksDispatcherOnMount: Dispatcher = {
20942146

20952147
useCallback: mountCallback,
20962148
useContext: readContext,
2149+
useSelectedContext: mountSelectedContext,
20972150
useEffect: mountEffect,
20982151
useImperativeHandle: mountImperativeHandle,
20992152
useLayoutEffect: mountLayoutEffect,
@@ -2119,6 +2172,7 @@ const HooksDispatcherOnUpdate: Dispatcher = {
21192172

21202173
useCallback: updateCallback,
21212174
useContext: readContext,
2175+
useSelectedContext: updateSelectedContext,
21222176
useEffect: updateEffect,
21232177
useImperativeHandle: updateImperativeHandle,
21242178
useLayoutEffect: updateLayoutEffect,
@@ -2144,6 +2198,7 @@ const HooksDispatcherOnRerender: Dispatcher = {
21442198

21452199
useCallback: updateCallback,
21462200
useContext: readContext,
2201+
useSelectedContext: updateSelectedContext,
21472202
useEffect: updateEffect,
21482203
useImperativeHandle: updateImperativeHandle,
21492204
useLayoutEffect: updateLayoutEffect,
@@ -2212,6 +2267,21 @@ if (__DEV__) {
22122267
mountHookTypesDev();
22132268
return readContext(context, observedBits);
22142269
},
2270+
useSelectedContext<C, S>(
2271+
context: ReactContext<C>,
2272+
selector: C => S,
2273+
isEqual: ((S, S) => boolean) | void,
2274+
): S {
2275+
currentHookNameInDev = 'useSelectedContext';
2276+
mountHookTypesDev();
2277+
const prevDispatcher = ReactCurrentDispatcher.current;
2278+
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
2279+
try {
2280+
return mountSelectedContext(context, selector, isEqual);
2281+
} finally {
2282+
ReactCurrentDispatcher.current = prevDispatcher;
2283+
}
2284+
},
22152285
useEffect(
22162286
create: () => (() => void) | void,
22172287
deps: Array<mixed> | void | null,
@@ -2346,6 +2416,21 @@ if (__DEV__) {
23462416
updateHookTypesDev();
23472417
return readContext(context, observedBits);
23482418
},
2419+
useSelectedContext<C, S>(
2420+
context: ReactContext<C>,
2421+
selector: C => S,
2422+
isEqual: ((S, S) => boolean) | void,
2423+
): S {
2424+
currentHookNameInDev = 'useSelectedContext';
2425+
updateHookTypesDev();
2426+
const prevDispatcher = ReactCurrentDispatcher.current;
2427+
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
2428+
try {
2429+
return mountSelectedContext(context, selector, isEqual);
2430+
} finally {
2431+
ReactCurrentDispatcher.current = prevDispatcher;
2432+
}
2433+
},
23492434
useEffect(
23502435
create: () => (() => void) | void,
23512436
deps: Array<mixed> | void | null,
@@ -2476,6 +2561,21 @@ if (__DEV__) {
24762561
updateHookTypesDev();
24772562
return readContext(context, observedBits);
24782563
},
2564+
useSelectedContext<C, S>(
2565+
context: ReactContext<C>,
2566+
selector: C => S,
2567+
isEqual: ((S, S) => boolean) | void,
2568+
): S {
2569+
currentHookNameInDev = 'useSelectedContext';
2570+
updateHookTypesDev();
2571+
const prevDispatcher = ReactCurrentDispatcher.current;
2572+
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
2573+
try {
2574+
return updateSelectedContext(context, selector, isEqual);
2575+
} finally {
2576+
ReactCurrentDispatcher.current = prevDispatcher;
2577+
}
2578+
},
24792579
useEffect(
24802580
create: () => (() => void) | void,
24812581
deps: Array<mixed> | void | null,
@@ -2607,6 +2707,21 @@ if (__DEV__) {
26072707
updateHookTypesDev();
26082708
return readContext(context, observedBits);
26092709
},
2710+
useSelectedContext<C, S>(
2711+
context: ReactContext<C>,
2712+
selector: C => S,
2713+
isEqual: ((S, S) => boolean) | void,
2714+
): S {
2715+
currentHookNameInDev = 'useSelectedContext';
2716+
updateHookTypesDev();
2717+
const prevDispatcher = ReactCurrentDispatcher.current;
2718+
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnRerenderInDEV;
2719+
try {
2720+
return updateSelectedContext(context, selector, isEqual);
2721+
} finally {
2722+
ReactCurrentDispatcher.current = prevDispatcher;
2723+
}
2724+
},
26102725
useEffect(
26112726
create: () => (() => void) | void,
26122727
deps: Array<mixed> | void | null,
@@ -2740,6 +2855,22 @@ if (__DEV__) {
27402855
mountHookTypesDev();
27412856
return readContext(context, observedBits);
27422857
},
2858+
useSelectedContext<C, S>(
2859+
context: ReactContext<C>,
2860+
selector: C => S,
2861+
isEqual: ((S, S) => boolean) | void,
2862+
): S {
2863+
currentHookNameInDev = 'useSelectedContext';
2864+
warnInvalidHookAccess();
2865+
mountHookTypesDev();
2866+
const prevDispatcher = ReactCurrentDispatcher.current;
2867+
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
2868+
try {
2869+
return mountSelectedContext(context, selector, isEqual);
2870+
} finally {
2871+
ReactCurrentDispatcher.current = prevDispatcher;
2872+
}
2873+
},
27432874
useEffect(
27442875
create: () => (() => void) | void,
27452876
deps: Array<mixed> | void | null,
@@ -2885,6 +3016,22 @@ if (__DEV__) {
28853016
updateHookTypesDev();
28863017
return readContext(context, observedBits);
28873018
},
3019+
useSelectedContext<C, S>(
3020+
context: ReactContext<C>,
3021+
selector: C => S,
3022+
isEqual: ((S, S) => boolean) | void,
3023+
): S {
3024+
currentHookNameInDev = 'useSelectedContext';
3025+
warnInvalidHookAccess();
3026+
updateHookTypesDev();
3027+
const prevDispatcher = ReactCurrentDispatcher.current;
3028+
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
3029+
try {
3030+
return updateSelectedContext(context, selector, isEqual);
3031+
} finally {
3032+
ReactCurrentDispatcher.current = prevDispatcher;
3033+
}
3034+
},
28883035
useEffect(
28893036
create: () => (() => void) | void,
28903037
deps: Array<mixed> | void | null,
@@ -3031,6 +3178,22 @@ if (__DEV__) {
30313178
updateHookTypesDev();
30323179
return readContext(context, observedBits);
30333180
},
3181+
useSelectedContext<C, S>(
3182+
context: ReactContext<C>,
3183+
selector: C => S,
3184+
isEqual: ((S, S) => boolean) | void,
3185+
): S {
3186+
currentHookNameInDev = 'useSelectedContext';
3187+
warnInvalidHookAccess();
3188+
updateHookTypesDev();
3189+
const prevDispatcher = ReactCurrentDispatcher.current;
3190+
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
3191+
try {
3192+
return updateSelectedContext(context, selector, isEqual);
3193+
} finally {
3194+
ReactCurrentDispatcher.current = prevDispatcher;
3195+
}
3196+
},
30343197
useEffect(
30353198
create: () => (() => void) | void,
30363199
deps: Array<mixed> | void | null,

0 commit comments

Comments
 (0)