Skip to content

Commit 887ee39

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 = 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.) 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 bde8c75 commit 887ee39

16 files changed

+515
-21
lines changed

Diff for: packages/react-debug-tools/src/ReactDebugHooks.js

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

132+
function useContextSelector<C, S>(
133+
context: ReactContext<C>,
134+
selector: C => S,
135+
): C {
136+
hookLog.push({
137+
primitive: 'ContextSelector',
138+
stackError: new Error(),
139+
value: context._currentValue,
140+
});
141+
return context._currentValue;
142+
}
143+
132144
function useState<S>(
133145
initialState: (() => S) | S,
134146
): [S, Dispatch<BasicStateAction<S>>] {
@@ -322,6 +334,7 @@ const Dispatcher: DispatcherType = {
322334
useCacheRefresh,
323335
useCallback,
324336
useContext,
337+
useContextSelector,
325338
useEffect,
326339
useImperativeHandle,
327340
useDebugValue,

Diff for: packages/react-dom/src/server/ReactPartialRendererHooks.js

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

254+
function useContextSelector<C, S>(
255+
context: ReactContext<C>,
256+
selector: C => S,
257+
): C {
258+
if (__DEV__) {
259+
currentHookNameInDev = 'useContextSelector';
260+
}
261+
resolveCurrentlyRenderingComponent();
262+
const threadID = currentPartialRenderer.threadID;
263+
validateContextBounds(context, threadID);
264+
return context[threadID];
265+
}
266+
254267
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
255268
// $FlowFixMe: Flow doesn't like mixed types
256269
return typeof action === 'function' ? action(state) : action;
@@ -503,6 +516,7 @@ export function setCurrentPartialRenderer(renderer: PartialRenderer) {
503516
export const Dispatcher: DispatcherType = {
504517
readContext,
505518
useContext,
519+
useContextSelector,
506520
useMemo,
507521
useReducer,
508522
useRef,

Diff for: packages/react-reconciler/src/ReactFiberHooks.new.js

+89-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,11 @@ import {
5555
higherLanePriority,
5656
DefaultLanePriority,
5757
} from './ReactFiberLane.new';
58-
import {readContext, checkIfContextChanged} from './ReactFiberNewContext.new';
58+
import {
59+
readContext,
60+
readContextWithSelector,
61+
checkIfContextChanged,
62+
} from './ReactFiberNewContext.new';
5963
import {HostRoot, CacheComponent} from './ReactWorkTags';
6064
import {
6165
Update as UpdateEffect,
@@ -2113,6 +2117,7 @@ export const ContextOnlyDispatcher: Dispatcher = {
21132117

21142118
useCallback: throwInvalidHookError,
21152119
useContext: throwInvalidHookError,
2120+
useContextSelector: throwInvalidHookError,
21162121
useEffect: throwInvalidHookError,
21172122
useImperativeHandle: throwInvalidHookError,
21182123
useLayoutEffect: throwInvalidHookError,
@@ -2138,6 +2143,7 @@ const HooksDispatcherOnMount: Dispatcher = {
21382143

21392144
useCallback: mountCallback,
21402145
useContext: readContext,
2146+
useContextSelector: readContextWithSelector,
21412147
useEffect: mountEffect,
21422148
useImperativeHandle: mountImperativeHandle,
21432149
useLayoutEffect: mountLayoutEffect,
@@ -2163,6 +2169,7 @@ const HooksDispatcherOnUpdate: Dispatcher = {
21632169

21642170
useCallback: updateCallback,
21652171
useContext: readContext,
2172+
useContextSelector: readContextWithSelector,
21662173
useEffect: updateEffect,
21672174
useImperativeHandle: updateImperativeHandle,
21682175
useLayoutEffect: updateLayoutEffect,
@@ -2188,6 +2195,7 @@ const HooksDispatcherOnRerender: Dispatcher = {
21882195

21892196
useCallback: updateCallback,
21902197
useContext: readContext,
2198+
useContextSelector: readContextWithSelector,
21912199
useEffect: updateEffect,
21922200
useImperativeHandle: updateImperativeHandle,
21932201
useLayoutEffect: updateLayoutEffect,
@@ -2256,6 +2264,17 @@ if (__DEV__) {
22562264
mountHookTypesDev();
22572265
return readContext(context, observedBits);
22582266
},
2267+
useContextSelector<C, S>(context: ReactContext<C>, selector: C => S): C {
2268+
currentHookNameInDev = 'useContextSelector';
2269+
mountHookTypesDev();
2270+
const prevDispatcher = ReactCurrentDispatcher.current;
2271+
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
2272+
try {
2273+
return readContextWithSelector(context, selector);
2274+
} finally {
2275+
ReactCurrentDispatcher.current = prevDispatcher;
2276+
}
2277+
},
22592278
useEffect(
22602279
create: () => (() => void) | void,
22612280
deps: Array<mixed> | void | null,
@@ -2390,6 +2409,17 @@ if (__DEV__) {
23902409
updateHookTypesDev();
23912410
return readContext(context, observedBits);
23922411
},
2412+
useContextSelector<C, S>(context: ReactContext<C>, selector: C => S): C {
2413+
currentHookNameInDev = 'useContextSelector';
2414+
updateHookTypesDev();
2415+
const prevDispatcher = ReactCurrentDispatcher.current;
2416+
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
2417+
try {
2418+
return readContextWithSelector(context, selector);
2419+
} finally {
2420+
ReactCurrentDispatcher.current = prevDispatcher;
2421+
}
2422+
},
23932423
useEffect(
23942424
create: () => (() => void) | void,
23952425
deps: Array<mixed> | void | null,
@@ -2520,6 +2550,17 @@ if (__DEV__) {
25202550
updateHookTypesDev();
25212551
return readContext(context, observedBits);
25222552
},
2553+
useContextSelector<C, S>(context: ReactContext<C>, selector: C => S): C {
2554+
currentHookNameInDev = 'useContextSelector';
2555+
updateHookTypesDev();
2556+
const prevDispatcher = ReactCurrentDispatcher.current;
2557+
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
2558+
try {
2559+
return readContextWithSelector(context, selector);
2560+
} finally {
2561+
ReactCurrentDispatcher.current = prevDispatcher;
2562+
}
2563+
},
25232564
useEffect(
25242565
create: () => (() => void) | void,
25252566
deps: Array<mixed> | void | null,
@@ -2651,6 +2692,17 @@ if (__DEV__) {
26512692
updateHookTypesDev();
26522693
return readContext(context, observedBits);
26532694
},
2695+
useContextSelector<C, S>(context: ReactContext<C>, selector: C => S): C {
2696+
currentHookNameInDev = 'useContextSelector';
2697+
updateHookTypesDev();
2698+
const prevDispatcher = ReactCurrentDispatcher.current;
2699+
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnRerenderInDEV;
2700+
try {
2701+
return readContextWithSelector(context, selector);
2702+
} finally {
2703+
ReactCurrentDispatcher.current = prevDispatcher;
2704+
}
2705+
},
26542706
useEffect(
26552707
create: () => (() => void) | void,
26562708
deps: Array<mixed> | void | null,
@@ -2784,6 +2836,18 @@ if (__DEV__) {
27842836
mountHookTypesDev();
27852837
return readContext(context, observedBits);
27862838
},
2839+
useContextSelector<C, S>(context: ReactContext<C>, selector: C => S): C {
2840+
currentHookNameInDev = 'useContextSelector';
2841+
warnInvalidHookAccess();
2842+
mountHookTypesDev();
2843+
const prevDispatcher = ReactCurrentDispatcher.current;
2844+
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
2845+
try {
2846+
return readContextWithSelector(context, selector);
2847+
} finally {
2848+
ReactCurrentDispatcher.current = prevDispatcher;
2849+
}
2850+
},
27872851
useEffect(
27882852
create: () => (() => void) | void,
27892853
deps: Array<mixed> | void | null,
@@ -2929,6 +2993,18 @@ if (__DEV__) {
29292993
updateHookTypesDev();
29302994
return readContext(context, observedBits);
29312995
},
2996+
useContextSelector<C, S>(context: ReactContext<C>, selector: C => S): C {
2997+
currentHookNameInDev = 'useContextSelector';
2998+
warnInvalidHookAccess();
2999+
updateHookTypesDev();
3000+
const prevDispatcher = ReactCurrentDispatcher.current;
3001+
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
3002+
try {
3003+
return readContextWithSelector(context, selector);
3004+
} finally {
3005+
ReactCurrentDispatcher.current = prevDispatcher;
3006+
}
3007+
},
29323008
useEffect(
29333009
create: () => (() => void) | void,
29343010
deps: Array<mixed> | void | null,
@@ -3075,6 +3151,18 @@ if (__DEV__) {
30753151
updateHookTypesDev();
30763152
return readContext(context, observedBits);
30773153
},
3154+
useContextSelector<C, S>(context: ReactContext<C>, selector: C => S): C {
3155+
currentHookNameInDev = 'useContextSelector';
3156+
warnInvalidHookAccess();
3157+
updateHookTypesDev();
3158+
const prevDispatcher = ReactCurrentDispatcher.current;
3159+
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
3160+
try {
3161+
return readContextWithSelector(context, selector);
3162+
} finally {
3163+
ReactCurrentDispatcher.current = prevDispatcher;
3164+
}
3165+
},
30783166
useEffect(
30793167
create: () => (() => void) | void,
30803168
deps: Array<mixed> | void | null,

0 commit comments

Comments
 (0)