Skip to content

Commit ae128b3

Browse files
committed
[Experiment] Context Selectors
For internal experimentation only. This implements `unstable_useContextSelector` behind a feature flag. It's based on [RFC 119](reactjs/rfcs#119) and [RFC 118](reactjs/rfcs#118) by @gnoff. Usage: ```js const context = useContextSelector(Context, c => c.selectedField); ``` 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.) One difference from the RFC is that it does not return the selected value. It returns the full context object. This serves a few purposes: it discourages you from creating any new objects or derived values inside the selector, because it'll get thrown out regardless. Instead, all the selector will do is return a subfield. Then you can compute the derived value inside the component, and if needed, you memoize that derived value with `useMemo`. If all the selectors do is access a subfield, they're (theoretically) fast enough that we can call them during the propagation scan and bail out really early, without having to visit the component during the render phase. Another benefit is that it's API compatible with `useContext`. So we can put it behind a flag that falls back to regular `useContext`. The longer term vision is that these optimizations (in addition to other memoization checks, like `useMemo` and `useCallback`) are inserted automatically by a compiler. So you would write code like this: ```js const {a, b} = useContext(Context); const derived = computeDerived(a, b); ``` and it would get converted to something like this: ```js const {a} = useContextSelector(Context, context => context.a); const {b} = useContextSelector(Context, context => context.b); const derived = useMemo(() => computeDerived(a, b), [a, b]); ``` (Though not this exactly. Some lower level compiler output target.)
1 parent ed429fc commit ae128b3

17 files changed

+522
-15
lines changed

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

+13
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,18 @@ function useContext<T>(context: ReactContext<T>): T {
123123
return context._currentValue;
124124
}
125125

126+
function useContextSelector<C, S>(
127+
context: ReactContext<C>,
128+
selector: C => S,
129+
): C {
130+
hookLog.push({
131+
primitive: 'ContextSelector',
132+
stackError: new Error(),
133+
value: context._currentValue,
134+
});
135+
return context._currentValue;
136+
}
137+
126138
function useState<S>(
127139
initialState: (() => S) | S,
128140
): [S, Dispatch<BasicStateAction<S>>] {
@@ -316,6 +328,7 @@ const Dispatcher: DispatcherType = {
316328
useCacheRefresh,
317329
useCallback,
318330
useContext,
331+
useContextSelector,
319332
useEffect,
320333
useImperativeHandle,
321334
useDebugValue,

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

+14
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,19 @@ function useContext<T>(context: ReactContext<T>): T {
245245
return context[threadID];
246246
}
247247

248+
function useContextSelector<C, S>(
249+
context: ReactContext<C>,
250+
selector: C => S,
251+
): C {
252+
if (__DEV__) {
253+
currentHookNameInDev = 'useContextSelector';
254+
}
255+
resolveCurrentlyRenderingComponent();
256+
const threadID = currentPartialRenderer.threadID;
257+
validateContextBounds(context, threadID);
258+
return context[threadID];
259+
}
260+
248261
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
249262
// $FlowFixMe: Flow doesn't like mixed types
250263
return typeof action === 'function' ? action(state) : action;
@@ -497,6 +510,7 @@ export function setCurrentPartialRenderer(renderer: PartialRenderer) {
497510
export const Dispatcher: DispatcherType = {
498511
readContext,
499512
useContext,
513+
useContextSelector,
500514
useMemo,
501515
useReducer,
502516
useRef,

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

+89-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,11 @@ import {
5656
setCurrentUpdatePriority,
5757
higherEventPriority,
5858
} from './ReactEventPriorities.new';
59-
import {readContext, checkIfContextChanged} from './ReactFiberNewContext.new';
59+
import {
60+
readContext,
61+
readContextWithSelector,
62+
checkIfContextChanged,
63+
} from './ReactFiberNewContext.new';
6064
import {HostRoot, CacheComponent} from './ReactWorkTags';
6165
import {
6266
LayoutStatic as LayoutStaticEffect,
@@ -2067,6 +2071,7 @@ export const ContextOnlyDispatcher: Dispatcher = {
20672071

20682072
useCallback: throwInvalidHookError,
20692073
useContext: throwInvalidHookError,
2074+
useContextSelector: throwInvalidHookError,
20702075
useEffect: throwInvalidHookError,
20712076
useImperativeHandle: throwInvalidHookError,
20722077
useLayoutEffect: throwInvalidHookError,
@@ -2092,6 +2097,7 @@ const HooksDispatcherOnMount: Dispatcher = {
20922097

20932098
useCallback: mountCallback,
20942099
useContext: readContext,
2100+
useContextSelector: readContextWithSelector,
20952101
useEffect: mountEffect,
20962102
useImperativeHandle: mountImperativeHandle,
20972103
useLayoutEffect: mountLayoutEffect,
@@ -2117,6 +2123,7 @@ const HooksDispatcherOnUpdate: Dispatcher = {
21172123

21182124
useCallback: updateCallback,
21192125
useContext: readContext,
2126+
useContextSelector: readContextWithSelector,
21202127
useEffect: updateEffect,
21212128
useImperativeHandle: updateImperativeHandle,
21222129
useLayoutEffect: updateLayoutEffect,
@@ -2142,6 +2149,7 @@ const HooksDispatcherOnRerender: Dispatcher = {
21422149

21432150
useCallback: updateCallback,
21442151
useContext: readContext,
2152+
useContextSelector: readContextWithSelector,
21452153
useEffect: updateEffect,
21462154
useImperativeHandle: updateImperativeHandle,
21472155
useLayoutEffect: updateLayoutEffect,
@@ -2204,6 +2212,17 @@ if (__DEV__) {
22042212
mountHookTypesDev();
22052213
return readContext(context);
22062214
},
2215+
useContextSelector<C, S>(context: ReactContext<C>, selector: C => S): C {
2216+
currentHookNameInDev = 'useContextSelector';
2217+
mountHookTypesDev();
2218+
const prevDispatcher = ReactCurrentDispatcher.current;
2219+
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
2220+
try {
2221+
return readContextWithSelector(context, selector);
2222+
} finally {
2223+
ReactCurrentDispatcher.current = prevDispatcher;
2224+
}
2225+
},
22072226
useEffect(
22082227
create: () => (() => void) | void,
22092228
deps: Array<mixed> | void | null,
@@ -2332,6 +2351,17 @@ if (__DEV__) {
23322351
updateHookTypesDev();
23332352
return readContext(context);
23342353
},
2354+
useContextSelector<C, S>(context: ReactContext<C>, selector: C => S): C {
2355+
currentHookNameInDev = 'useContextSelector';
2356+
updateHookTypesDev();
2357+
const prevDispatcher = ReactCurrentDispatcher.current;
2358+
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
2359+
try {
2360+
return readContextWithSelector(context, selector);
2361+
} finally {
2362+
ReactCurrentDispatcher.current = prevDispatcher;
2363+
}
2364+
},
23352365
useEffect(
23362366
create: () => (() => void) | void,
23372367
deps: Array<mixed> | void | null,
@@ -2456,6 +2486,17 @@ if (__DEV__) {
24562486
updateHookTypesDev();
24572487
return readContext(context);
24582488
},
2489+
useContextSelector<C, S>(context: ReactContext<C>, selector: C => S): C {
2490+
currentHookNameInDev = 'useContextSelector';
2491+
updateHookTypesDev();
2492+
const prevDispatcher = ReactCurrentDispatcher.current;
2493+
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
2494+
try {
2495+
return readContextWithSelector(context, selector);
2496+
} finally {
2497+
ReactCurrentDispatcher.current = prevDispatcher;
2498+
}
2499+
},
24592500
useEffect(
24602501
create: () => (() => void) | void,
24612502
deps: Array<mixed> | void | null,
@@ -2581,6 +2622,17 @@ if (__DEV__) {
25812622
updateHookTypesDev();
25822623
return readContext(context);
25832624
},
2625+
useContextSelector<C, S>(context: ReactContext<C>, selector: C => S): C {
2626+
currentHookNameInDev = 'useContextSelector';
2627+
updateHookTypesDev();
2628+
const prevDispatcher = ReactCurrentDispatcher.current;
2629+
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnRerenderInDEV;
2630+
try {
2631+
return readContextWithSelector(context, selector);
2632+
} finally {
2633+
ReactCurrentDispatcher.current = prevDispatcher;
2634+
}
2635+
},
25842636
useEffect(
25852637
create: () => (() => void) | void,
25862638
deps: Array<mixed> | void | null,
@@ -2708,6 +2760,18 @@ if (__DEV__) {
27082760
mountHookTypesDev();
27092761
return readContext(context);
27102762
},
2763+
useContextSelector<C, S>(context: ReactContext<C>, selector: C => S): C {
2764+
currentHookNameInDev = 'useContextSelector';
2765+
warnInvalidHookAccess();
2766+
mountHookTypesDev();
2767+
const prevDispatcher = ReactCurrentDispatcher.current;
2768+
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
2769+
try {
2770+
return readContextWithSelector(context, selector);
2771+
} finally {
2772+
ReactCurrentDispatcher.current = prevDispatcher;
2773+
}
2774+
},
27112775
useEffect(
27122776
create: () => (() => void) | void,
27132777
deps: Array<mixed> | void | null,
@@ -2847,6 +2911,18 @@ if (__DEV__) {
28472911
updateHookTypesDev();
28482912
return readContext(context);
28492913
},
2914+
useContextSelector<C, S>(context: ReactContext<C>, selector: C => S): C {
2915+
currentHookNameInDev = 'useContextSelector';
2916+
warnInvalidHookAccess();
2917+
updateHookTypesDev();
2918+
const prevDispatcher = ReactCurrentDispatcher.current;
2919+
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
2920+
try {
2921+
return readContextWithSelector(context, selector);
2922+
} finally {
2923+
ReactCurrentDispatcher.current = prevDispatcher;
2924+
}
2925+
},
28502926
useEffect(
28512927
create: () => (() => void) | void,
28522928
deps: Array<mixed> | void | null,
@@ -2987,6 +3063,18 @@ if (__DEV__) {
29873063
updateHookTypesDev();
29883064
return readContext(context);
29893065
},
3066+
useContextSelector<C, S>(context: ReactContext<C>, selector: C => S): C {
3067+
currentHookNameInDev = 'useContextSelector';
3068+
warnInvalidHookAccess();
3069+
updateHookTypesDev();
3070+
const prevDispatcher = ReactCurrentDispatcher.current;
3071+
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
3072+
try {
3073+
return readContextWithSelector(context, selector);
3074+
} finally {
3075+
ReactCurrentDispatcher.current = prevDispatcher;
3076+
}
3077+
},
29903078
useEffect(
29913079
create: () => (() => void) | void,
29923080
deps: Array<mixed> | void | null,

0 commit comments

Comments
 (0)