Skip to content

Commit

Permalink
[Experiment] Context Selectors
Browse files Browse the repository at this point in the history
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.)
  • Loading branch information
acdlite committed Jul 7, 2021
1 parent 9c7f29e commit 235649f
Show file tree
Hide file tree
Showing 16 changed files with 509 additions and 15 deletions.
13 changes: 13 additions & 0 deletions packages/react-debug-tools/src/ReactDebugHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,18 @@ function useContext<T>(context: ReactContext<T>): T {
return context._currentValue;
}

function useContextSelector<C, S>(
context: ReactContext<C>,
selector: C => S,
): C {
hookLog.push({
primitive: 'ContextSelector',
stackError: new Error(),
value: context._currentValue,
});
return context._currentValue;
}

function useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
Expand Down Expand Up @@ -316,6 +328,7 @@ const Dispatcher: DispatcherType = {
useCacheRefresh,
useCallback,
useContext,
useContextSelector,
useEffect,
useImperativeHandle,
useDebugValue,
Expand Down
14 changes: 14 additions & 0 deletions packages/react-dom/src/server/ReactPartialRendererHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,19 @@ function useContext<T>(context: ReactContext<T>): T {
return context[threadID];
}

function useContextSelector<C, S>(
context: ReactContext<C>,
selector: C => S,
): C {
if (__DEV__) {
currentHookNameInDev = 'useContextSelector';
}
resolveCurrentlyRenderingComponent();
const threadID = currentPartialRenderer.threadID;
validateContextBounds(context, threadID);
return context[threadID];
}

function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
// $FlowFixMe: Flow doesn't like mixed types
return typeof action === 'function' ? action(state) : action;
Expand Down Expand Up @@ -497,6 +510,7 @@ export function setCurrentPartialRenderer(renderer: PartialRenderer) {
export const Dispatcher: DispatcherType = {
readContext,
useContext,
useContextSelector,
useMemo,
useReducer,
useRef,
Expand Down
90 changes: 89 additions & 1 deletion packages/react-reconciler/src/ReactFiberHooks.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,11 @@ import {
setCurrentUpdatePriority,
higherEventPriority,
} from './ReactEventPriorities.new';
import {readContext, checkIfContextChanged} from './ReactFiberNewContext.new';
import {
readContext,
readContextWithSelector,
checkIfContextChanged,
} from './ReactFiberNewContext.new';
import {HostRoot, CacheComponent} from './ReactWorkTags';
import {
LayoutStatic as LayoutStaticEffect,
Expand Down Expand Up @@ -2067,6 +2071,7 @@ export const ContextOnlyDispatcher: Dispatcher = {

useCallback: throwInvalidHookError,
useContext: throwInvalidHookError,
useContextSelector: throwInvalidHookError,
useEffect: throwInvalidHookError,
useImperativeHandle: throwInvalidHookError,
useLayoutEffect: throwInvalidHookError,
Expand All @@ -2092,6 +2097,7 @@ const HooksDispatcherOnMount: Dispatcher = {

useCallback: mountCallback,
useContext: readContext,
useContextSelector: readContextWithSelector,
useEffect: mountEffect,
useImperativeHandle: mountImperativeHandle,
useLayoutEffect: mountLayoutEffect,
Expand All @@ -2117,6 +2123,7 @@ const HooksDispatcherOnUpdate: Dispatcher = {

useCallback: updateCallback,
useContext: readContext,
useContextSelector: readContextWithSelector,
useEffect: updateEffect,
useImperativeHandle: updateImperativeHandle,
useLayoutEffect: updateLayoutEffect,
Expand All @@ -2142,6 +2149,7 @@ const HooksDispatcherOnRerender: Dispatcher = {

useCallback: updateCallback,
useContext: readContext,
useContextSelector: readContextWithSelector,
useEffect: updateEffect,
useImperativeHandle: updateImperativeHandle,
useLayoutEffect: updateLayoutEffect,
Expand Down Expand Up @@ -2204,6 +2212,17 @@ if (__DEV__) {
mountHookTypesDev();
return readContext(context);
},
useContextSelector<C, S>(context: ReactContext<C>, selector: C => S): C {
currentHookNameInDev = 'useContextSelector';
mountHookTypesDev();
const prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
try {
return readContextWithSelector(context, selector);
} finally {
ReactCurrentDispatcher.current = prevDispatcher;
}
},
useEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
Expand Down Expand Up @@ -2332,6 +2351,17 @@ if (__DEV__) {
updateHookTypesDev();
return readContext(context);
},
useContextSelector<C, S>(context: ReactContext<C>, selector: C => S): C {
currentHookNameInDev = 'useContextSelector';
updateHookTypesDev();
const prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
try {
return readContextWithSelector(context, selector);
} finally {
ReactCurrentDispatcher.current = prevDispatcher;
}
},
useEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
Expand Down Expand Up @@ -2456,6 +2486,17 @@ if (__DEV__) {
updateHookTypesDev();
return readContext(context);
},
useContextSelector<C, S>(context: ReactContext<C>, selector: C => S): C {
currentHookNameInDev = 'useContextSelector';
updateHookTypesDev();
const prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
try {
return readContextWithSelector(context, selector);
} finally {
ReactCurrentDispatcher.current = prevDispatcher;
}
},
useEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
Expand Down Expand Up @@ -2581,6 +2622,17 @@ if (__DEV__) {
updateHookTypesDev();
return readContext(context);
},
useContextSelector<C, S>(context: ReactContext<C>, selector: C => S): C {
currentHookNameInDev = 'useContextSelector';
updateHookTypesDev();
const prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnRerenderInDEV;
try {
return readContextWithSelector(context, selector);
} finally {
ReactCurrentDispatcher.current = prevDispatcher;
}
},
useEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
Expand Down Expand Up @@ -2708,6 +2760,18 @@ if (__DEV__) {
mountHookTypesDev();
return readContext(context);
},
useContextSelector<C, S>(context: ReactContext<C>, selector: C => S): C {
currentHookNameInDev = 'useContextSelector';
warnInvalidHookAccess();
mountHookTypesDev();
const prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
try {
return readContextWithSelector(context, selector);
} finally {
ReactCurrentDispatcher.current = prevDispatcher;
}
},
useEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
Expand Down Expand Up @@ -2847,6 +2911,18 @@ if (__DEV__) {
updateHookTypesDev();
return readContext(context);
},
useContextSelector<C, S>(context: ReactContext<C>, selector: C => S): C {
currentHookNameInDev = 'useContextSelector';
warnInvalidHookAccess();
updateHookTypesDev();
const prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
try {
return readContextWithSelector(context, selector);
} finally {
ReactCurrentDispatcher.current = prevDispatcher;
}
},
useEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
Expand Down Expand Up @@ -2987,6 +3063,18 @@ if (__DEV__) {
updateHookTypesDev();
return readContext(context);
},
useContextSelector<C, S>(context: ReactContext<C>, selector: C => S): C {
currentHookNameInDev = 'useContextSelector';
warnInvalidHookAccess();
updateHookTypesDev();
const prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
try {
return readContextWithSelector(context, selector);
} finally {
ReactCurrentDispatcher.current = prevDispatcher;
}
},
useEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
Expand Down
Loading

0 comments on commit 235649f

Please sign in to comment.