Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Experiment] Context Selectors #20646

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

acdlite
Copy link
Collaborator

@acdlite acdlite commented Jan 23, 2021

Based on #20890

This is not a final API. It's meant for internal experimentation only. If we land this feature in our stable release channel, it will likely differ from the version presented here.

This implements unstable_useContextSelector behind a feature flag. It's based on RFC 119 and RFC 118 by @gnoff.

Usage:

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:

const {a, b} = useContext(Context);
const derived = computeDerived(a, b);

and it would get converted to something like this:

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.)

@facebook-github-bot facebook-github-bot added CLA Signed React Core Team Opened by a member of the React Core Team labels Jan 23, 2021
@codesandbox-ci
Copy link

codesandbox-ci bot commented Jan 23, 2021

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 26d596d:

Sandbox Source
React Configuration

@sizebot
Copy link

sizebot commented Jan 23, 2021

Comparing: 903384a...eb46705

Critical size changes

Includes critical production bundles, as well as any change greater
than 2%:

Name +/- Base Current +/- gzip Base gzip Current gzip
oss-experimental/react-debug-tools/cjs/react-debug-tools.production.min.js +2.09% 6.17 kB 6.29 kB +0.96% 2.41 kB 2.43 kB
oss-stable/react-debug-tools/cjs/react-debug-tools.production.min.js +2.09% 6.17 kB 6.29 kB +0.96% 2.41 kB 2.43 kB
oss-experimental/react-dom/cjs/react-dom.production.min.js +0.43% 128.86 kB 129.41 kB +0.31% 41.52 kB 41.65 kB
facebook-www/ReactDOM-prod.modern.js +0.35% 393.99 kB 395.35 kB +0.29% 73.43 kB 73.65 kB
facebook-www/ReactDOM-prod.classic.js +0.34% 405.64 kB 407.00 kB +0.29% 75.23 kB 75.45 kB
facebook-www/ReactDOMForked-prod.classic.js +0.34% 405.65 kB 407.01 kB +0.29% 75.24 kB 75.45 kB
oss-stable/react-dom/cjs/react-dom.production.min.js +0.11% 122.28 kB 122.42 kB -0.02% 39.49 kB 39.48 kB

Significant size changes

Includes any change greater than 0.2%:

Expand to show
Name +/- Base Current +/- gzip Base gzip Current gzip
oss-experimental/react-debug-tools/cjs/react-debug-tools.production.min.js +2.09% 6.17 kB 6.29 kB +0.96% 2.41 kB 2.43 kB
oss-stable/react-debug-tools/cjs/react-debug-tools.production.min.js +2.09% 6.17 kB 6.29 kB +0.96% 2.41 kB 2.43 kB
oss-experimental/react-suspense-test-utils/cjs/react-suspense-test-utils.js +1.44% 2.57 kB 2.61 kB +0.65% 1.08 kB 1.09 kB
oss-stable/react-suspense-test-utils/cjs/react-suspense-test-utils.js +1.44% 2.57 kB 2.61 kB +0.65% 1.08 kB 1.09 kB
oss-experimental/react-debug-tools/cjs/react-debug-tools.development.js +1.44% 21.31 kB 21.61 kB +1.13% 5.84 kB 5.91 kB
oss-stable/react-debug-tools/cjs/react-debug-tools.development.js +1.44% 21.31 kB 21.61 kB +1.13% 5.84 kB 5.91 kB
oss-experimental/react/cjs/react.development.js +1.30% 74.31 kB 75.27 kB +0.38% 19.86 kB 19.94 kB
oss-experimental/react/cjs/react.production.min.js +1.27% 7.59 kB 7.68 kB +0.58% 2.95 kB 2.97 kB
facebook-react-native/react/cjs/React-dev.js +1.15% 89.27 kB 90.30 kB +0.43% 21.40 kB 21.49 kB
facebook-react-native/react/cjs/React-prod.js +1.10% 16.79 kB 16.97 kB +0.79% 4.31 kB 4.34 kB
facebook-react-native/react/cjs/React-profiling.js +1.10% 16.79 kB 16.97 kB +0.79% 4.31 kB 4.34 kB
facebook-www/React-prod.modern.js +1.09% 16.95 kB 17.13 kB +0.82% 4.38 kB 4.42 kB
facebook-www/React-profiling.modern.js +1.09% 16.95 kB 17.13 kB +0.82% 4.38 kB 4.42 kB
facebook-www/React-prod.classic.js +1.08% 17.09 kB 17.28 kB +0.84% 4.43 kB 4.46 kB
facebook-www/React-profiling.classic.js +1.08% 17.09 kB 17.28 kB +0.84% 4.43 kB 4.46 kB
facebook-www/React-dev.modern.js +1.05% 98.04 kB 99.06 kB +0.41% 23.86 kB 23.96 kB
facebook-www/React-dev.classic.js +1.04% 99.05 kB 100.08 kB +0.43% 24.09 kB 24.19 kB
oss-experimental/react-test-renderer/cjs/react-test-renderer.development.js +1.00% 623.33 kB 629.54 kB +0.56% 136.39 kB 137.15 kB
oss-experimental/react-test-renderer/umd/react-test-renderer.development.js +1.00% 654.35 kB 660.86 kB +0.58% 138.02 kB 138.82 kB
oss-experimental/react-art/cjs/react-art.development.js +0.97% 638.71 kB 644.93 kB +0.58% 139.09 kB 139.89 kB
facebook-www/ReactART-dev.modern.js +0.92% 684.09 kB 690.35 kB +0.55% 145.32 kB 146.11 kB
facebook-www/ReactART-dev.classic.js +0.90% 694.34 kB 700.60 kB +0.54% 147.40 kB 148.21 kB
oss-experimental/react-reconciler/cjs/react-reconciler.development.js +0.90% 697.36 kB 703.61 kB +0.53% 149.19 kB 149.98 kB
oss-experimental/react/umd/react.development.js +0.89% 111.75 kB 112.74 kB +0.29% 27.36 kB 27.44 kB
oss-experimental/react-art/umd/react-art.development.js +0.88% 741.50 kB 748.02 kB +0.51% 157.63 kB 158.43 kB
oss-experimental/react/umd/react.production.min.js +0.75% 12.03 kB 12.12 kB +0.30% 4.71 kB 4.73 kB
oss-stable/react-art/cjs/react-art.development.js +0.69% 597.52 kB 601.66 kB +0.31% 130.63 kB 131.03 kB
oss-stable/react-test-renderer/cjs/react-test-renderer.development.js +0.68% 607.12 kB 611.26 kB +0.30% 132.96 kB 133.36 kB
oss-stable/react-test-renderer/umd/react-test-renderer.development.js +0.68% 637.29 kB 641.63 kB +0.28% 134.59 kB 134.97 kB
oss-experimental/react-art/cjs/react-art.production.min.js +0.68% 81.71 kB 82.26 kB +0.37% 25.60 kB 25.70 kB
oss-experimental/react-test-renderer/cjs/react-test-renderer.production.min.js +0.68% 82.02 kB 82.58 kB +0.45% 25.73 kB 25.84 kB
oss-experimental/react-test-renderer/umd/react-test-renderer.production.min.js +0.67% 82.21 kB 82.76 kB +0.46% 26.09 kB 26.21 kB
facebook-react-native/react-test-renderer/cjs/ReactTestRenderer-dev.js +0.67% 617.99 kB 622.12 kB +0.30% 133.99 kB 134.39 kB
facebook-www/ReactDOMTesting-dev.modern.js +0.66% 947.16 kB 953.42 kB +0.39% 213.27 kB 214.09 kB
facebook-www/ReactTestRenderer-dev.classic.js +0.66% 630.56 kB 634.69 kB +0.29% 135.37 kB 135.77 kB
facebook-www/ReactTestRenderer-dev.modern.js +0.66% 630.57 kB 634.71 kB +0.29% 135.38 kB 135.78 kB
oss-experimental/react-dom/cjs/react-dom.development.js +0.65% 961.76 kB 967.98 kB +0.35% 218.00 kB 218.77 kB
oss-experimental/react-dom/umd/react-dom.development.js +0.64% 1,010.56 kB 1,017.07 kB +0.35% 221.01 kB 221.78 kB
facebook-www/ReactDOMTesting-dev.classic.js +0.64% 976.18 kB 982.44 kB +0.37% 219.08 kB 219.89 kB
oss-stable/react-reconciler/cjs/react-reconciler.development.js +0.63% 653.50 kB 657.64 kB +0.29% 140.22 kB 140.63 kB
oss-experimental/react-reconciler/cjs/react-reconciler.production.min.js +0.63% 91.98 kB 92.56 kB +0.14% 28.60 kB 28.64 kB
oss-stable/react-art/umd/react-art.development.js +0.62% 698.24 kB 702.58 kB +0.27% 148.98 kB 149.38 kB
facebook-www/ReactDOM-dev.modern.js +0.61% 1,025.58 kB 1,031.85 kB +0.34% 227.96 kB 228.73 kB
facebook-www/ReactDOMForked-dev.modern.js +0.61% 1,025.59 kB 1,031.85 kB +0.34% 227.75 kB 228.53 kB
react-native/implementations/ReactFabric-dev.js +0.60% 692.90 kB 697.04 kB +0.27% 149.72 kB 150.12 kB
facebook-www/ReactDOM-dev.classic.js +0.60% 1,051.78 kB 1,058.04 kB +0.34% 232.91 kB 233.69 kB
facebook-www/ReactDOMForked-dev.classic.js +0.60% 1,051.79 kB 1,058.05 kB +0.34% 232.71 kB 233.49 kB
react-native/implementations/ReactFabric-dev.fb.js +0.59% 698.42 kB 702.56 kB +0.27% 150.59 kB 150.99 kB
oss-experimental/react-reconciler/cjs/react-reconciler.profiling.min.js +0.59% 97.90 kB 98.48 kB +0.30% 30.39 kB 30.48 kB
oss-experimental/react/umd/react.profiling.min.js +0.58% 15.63 kB 15.72 kB +0.29% 5.82 kB 5.83 kB
react-native/implementations/ReactNativeRenderer-dev.js +0.58% 711.86 kB 715.99 kB +0.26% 154.43 kB 154.84 kB
react-native/implementations/ReactNativeRenderer-dev.fb.js +0.58% 717.37 kB 721.50 kB +0.27% 155.30 kB 155.73 kB
facebook-www/ReactART-prod.modern.js +0.54% 253.57 kB 254.93 kB +0.47% 45.26 kB 45.47 kB
facebook-www/ReactDOMServer-prod.modern.js +0.52% 47.51 kB 47.76 kB +0.32% 11.07 kB 11.10 kB
facebook-www/ReactART-prod.classic.js +0.52% 260.92 kB 262.28 kB +0.48% 46.57 kB 46.80 kB
facebook-www/ReactDOMServer-prod.classic.js +0.51% 48.40 kB 48.65 kB +0.41% 11.28 kB 11.33 kB
oss-experimental/react-art/umd/react-art.production.min.js +0.47% 117.61 kB 118.16 kB +0.40% 36.68 kB 36.83 kB
oss-stable/react-dom/cjs/react-dom.development.js +0.45% 910.53 kB 914.67 kB +0.19% 207.92 kB 208.32 kB
oss-stable/react-dom/umd/react-dom.development.js +0.45% 956.76 kB 961.11 kB +0.17% 210.68 kB 211.05 kB
oss-experimental/react-dom/cjs/react-dom.production.min.js +0.43% 128.86 kB 129.41 kB +0.31% 41.52 kB 41.65 kB
oss-experimental/react-dom/umd/react-dom.production.min.js +0.43% 128.69 kB 129.24 kB +0.27% 42.20 kB 42.32 kB
oss-experimental/react-dom/cjs/react-dom.profiling.min.js +0.41% 134.86 kB 135.42 kB +0.24% 43.30 kB 43.40 kB
oss-experimental/react-dom/umd/react-dom.profiling.min.js +0.41% 134.52 kB 135.07 kB +0.26% 43.94 kB 44.05 kB
facebook-www/ReactDOMTesting-prod.modern.js +0.40% 386.88 kB 388.41 kB +0.33% 73.39 kB 73.63 kB
oss-stable/react-dom/cjs/react-dom-server.browser.production.min.js +0.39% 20.17 kB 20.25 kB +0.25% 7.57 kB 7.59 kB
facebook-www/ReactDOMTesting-prod.classic.js +0.38% 400.41 kB 401.94 kB +0.30% 75.59 kB 75.82 kB
oss-stable/react-dom/cjs/react-dom-server.node.production.min.js +0.38% 20.61 kB 20.68 kB +0.25% 7.73 kB 7.75 kB
oss-experimental/react-dom/cjs/react-dom-server.browser.production.min.js +0.37% 20.73 kB 20.81 kB +0.26% 7.69 kB 7.71 kB
oss-stable/react-dom/umd/react-dom-server.browser.production.min.js +0.37% 20.27 kB 20.35 kB +0.07% 7.61 kB 7.62 kB
oss-experimental/react-dom/cjs/react-dom-server.node.production.min.js +0.36% 21.16 kB 21.24 kB +0.27% 7.85 kB 7.87 kB
oss-experimental/react-dom/umd/react-dom-server.browser.production.min.js +0.36% 20.83 kB 20.91 kB +0.13% 7.73 kB 7.74 kB
facebook-www/ReactDOM-prod.modern.js +0.35% 393.99 kB 395.35 kB +0.29% 73.43 kB 73.65 kB
facebook-www/ReactDOMForked-prod.modern.js +0.35% 394.00 kB 395.36 kB +0.29% 73.44 kB 73.66 kB
facebook-www/ReactDOM-prod.classic.js +0.34% 405.64 kB 407.00 kB +0.29% 75.23 kB 75.45 kB
facebook-www/ReactDOMForked-prod.classic.js +0.34% 405.65 kB 407.01 kB +0.29% 75.24 kB 75.45 kB
oss-experimental/react-server/cjs/react-server-flight.production.min.js +0.34% 6.55 kB 6.57 kB +0.18% 2.77 kB 2.78 kB
oss-stable/react-server/cjs/react-server-flight.production.min.js +0.34% 6.55 kB 6.57 kB +0.18% 2.77 kB 2.78 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-writer.browser.production.min.server.js +0.33% 6.31 kB 6.33 kB +0.19% 2.70 kB 2.70 kB
oss-stable/react-server-dom-webpack/cjs/react-server-dom-webpack-writer.browser.production.min.server.js +0.33% 6.31 kB 6.33 kB +0.19% 2.70 kB 2.70 kB
facebook-www/ReactDOM-profiling.modern.js +0.33% 412.82 kB 414.18 kB +0.32% 76.69 kB 76.94 kB
facebook-www/ReactDOMForked-profiling.modern.js +0.33% 412.83 kB 414.19 kB +0.32% 76.70 kB 76.95 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-writer.node.production.min.server.js +0.32% 6.51 kB 6.53 kB +0.22% 2.73 kB 2.73 kB
oss-stable/react-server-dom-webpack/cjs/react-server-dom-webpack-writer.node.production.min.server.js +0.32% 6.51 kB 6.53 kB +0.22% 2.73 kB 2.73 kB
oss-experimental/react-server-dom-webpack/umd/react-server-dom-webpack-writer.browser.production.min.server.js +0.32% 6.53 kB 6.55 kB +0.18% 2.79 kB 2.80 kB
oss-stable/react-server-dom-webpack/umd/react-server-dom-webpack-writer.browser.production.min.server.js +0.32% 6.53 kB 6.55 kB +0.18% 2.79 kB 2.80 kB
facebook-www/ReactDOM-profiling.classic.js +0.32% 424.52 kB 425.88 kB +0.32% 78.51 kB 78.77 kB
facebook-www/ReactDOMForked-profiling.classic.js +0.32% 424.53 kB 425.89 kB +0.32% 78.52 kB 78.77 kB
facebook-www/ReactFlightDOMRelayServer-prod.classic.js +0.29% 14.37 kB 14.41 kB +0.19% 3.76 kB 3.77 kB
facebook-www/ReactFlightDOMRelayServer-prod.modern.js +0.29% 14.37 kB 14.41 kB +0.19% 3.76 kB 3.77 kB
oss-stable/react-dom/umd/react-dom-server.browser.development.js +0.28% 145.87 kB 146.28 kB +0.19% 37.33 kB 37.40 kB
oss-stable/react-dom/cjs/react-dom-server.browser.development.js +0.28% 138.37 kB 138.76 kB +0.22% 36.86 kB 36.94 kB
oss-experimental/react-dom/umd/react-dom-server.browser.development.js +0.28% 147.82 kB 148.23 kB +0.18% 37.59 kB 37.66 kB
oss-stable/react-dom/cjs/react-dom-server.node.development.js +0.28% 139.67 kB 140.06 kB +0.21% 37.12 kB 37.20 kB
facebook-relay/flight/ReactFlightNativeRelayServer-prod.js +0.28% 14.84 kB 14.88 kB +0.21% 3.89 kB 3.89 kB
oss-experimental/react-dom/cjs/react-dom-server.browser.development.js +0.28% 140.21 kB 140.60 kB +0.21% 37.15 kB 37.22 kB
oss-experimental/react-dom/cjs/react-dom-server.node.development.js +0.27% 141.51 kB 141.90 kB +0.21% 37.41 kB 37.48 kB
facebook-www/ReactDOMServer-dev.modern.js +0.27% 145.23 kB 145.62 kB +0.17% 37.28 kB 37.34 kB
facebook-www/ReactDOMServer-dev.classic.js +0.26% 149.36 kB 149.74 kB +0.17% 38.32 kB 38.38 kB

Generated by 🚫 dangerJS against eb46705

@acdlite acdlite marked this pull request as ready for review January 23, 2021 06:36
@markerikson
Copy link
Contributor

Obvious question:

why useSelectedContext, and not useContextSelector?

@dai-shi
Copy link
Contributor

dai-shi commented Jan 23, 2021

lol, I was waiting somebody ask.
Although I understand the intuition, what it's supposed to be is useSelectedContextValue which is too long.

In one of the RFCs, there was a discussion about the idea of useContext(ctx, selectorFn). Certainly, this requires to drop unstable_observedBits. Might look cleaner. (Personally, I'm ok with whatever decision is made.)

@acdlite
Copy link
Collaborator Author

acdlite commented Jan 23, 2021

Think of this PR as a proof of concept. It’s very unlikely to be the final design. We can bikeshed more before release.

Re: why it’s a separate hook , makes it easier to track usages internally, and delete if needed. Also avoids a conflict with observed bits, which we still need to remove.

This also isn’t the only context-related feature we have planned, and it’s unclear how they’ll overlap. Might be separate hooks, might be all the same hook.

@markerikson
Copy link
Contributor

Gotcha. Out of curiosity, any chance of an RFC or something discussing the other plans for context?

@acdlite
Copy link
Collaborator Author

acdlite commented Jan 23, 2021

When they’re more fleshed out, yeah. One of the motivations for this PR was that the other proposals are only useful in combination with this feature (bailing out during render if nothing has changed).

@lxsmnsyc
Copy link

lxsmnsyc commented Jan 25, 2021

In one of the RFCs, there was a discussion about the idea of useContext(ctx, selectorFn). Certainly, this requires to drop unstable_observedBits. Might look cleaner. (Personally, I'm ok with whatever decision is made.)

I think this is better, we can easily bail in/out of the selector functionality anytime.

Could also imagine the API being like this:

declare function useContext<T>(ctx: Context<T>): T;
declare function useContext<T, R>(ctx: Context<T>, selector: (value: T) => R, isEqual?: (prev: R, next: R) => boolean): R;

Comment on lines 56 to 57
const a = useSelectedContext(Context, context => context.b);
return <Text text={a} />;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const a = useSelectedContext(Context, context => context.b);
return <Text text={a} />;
const b = useSelectedContext(Context, context => context.b);
return <Text text={b} />;

@acdlite
Copy link
Collaborator Author

acdlite commented Jan 25, 2021

Let's keep the discussion at RFC 119 and RFC 118.

@facebook facebook locked and limited conversation to collaborators Jan 25, 2021
Copy link
Collaborator

@gnoff gnoff left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@acdlite excited to see you pick this up :) lmk if you want to discuss anything related to reactjs/rfcs#118 and reactjs/rfcs#150 when you start looking into optimizations as a whole

function updateSelectedContext<C, S>(
Context: ReactContext<C>,
selector: C => S,
isEqual: ((S, S) => boolean) | void,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is interesting. When I wrote my initial PoC PR I thought about this but excluded it given other hooks don't expose these sorts of overrides and you can easily "break" things with it. Is this something you think might make it into a final implementation? My general impression was hooks should not allow you to be wrong if it can help it

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The motivating use case was selecting multiple values. Like a tuple or record. That would instantly break the Object.is memoization.

An alternative design would be to pass the previous context value to the selector, so that the selector can choose to return the old one if desired.

I think I like that design more but went with this one for now as a strawman.

Comment on lines 254 to 256
// TODO: We could call the selector right here, during propagation.
// That would give us the opportunity to bail out early, without
// even visiting the fiber.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to avoid this by also implementing changes in reactjs/rfcs#118 because you end up with the ability to run expensive selectors multiple times if there is intermediate work that causes a bailed out fiber to end up rendering anyway.

I have no idea if "expensive selectors" is a real worry of course and so you're likely right that it wouldn't matter a whole lot perf wise.

There is one more edge case that this would trip up on though and that is if a deeper component with a selector is going to get props from an ancestor that will update on the same context change you can run the deeper selector with props that came from an earlier version of the context value.

For instance if you have a context with a key a and an ancestor component sees a and passes it to a deeper component which uses it to select from the context. then on a context value update a is deleted and b is the key. the ancestor will eventually pass b to the deeper child but the selector call here during propagation would try to read a and get an error.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to avoid this by also implementing changes in reactjs/rfcs#118 because you end up with the ability to run expensive selectors multiple times if there is intermediate work that causes a bailed out fiber to end up rendering anyway.

Yeah that's why I left it as a TODO 👍 Just wanted the comment there for future reference. Also why I called the dependency field "hook" since it's likely we'll need an actual reference to the current hook there, rather than a boolean.

@facebook facebook unlocked this conversation Feb 6, 2021
@facebook facebook locked as off-topic and limited conversation to collaborators Feb 6, 2021
@facebook facebook deleted a comment from sizebot Feb 6, 2021
@acdlite acdlite force-pushed the context-selectors branch 2 times, most recently from 1a3b6b2 to a22f61c Compare February 11, 2021 04:22
@acdlite acdlite changed the title Implement naive version of context selectors [Experiment] Context Selectors Feb 26, 2021
@acdlite
Copy link
Collaborator Author

acdlite commented Feb 26, 2021

Pushed some updates. It's now based on top of the Lazy Propagation (#20890) experiment.

I also modified the API so that it returns the full context object, instead of a selected value.

Added rationale to PR description:

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:

const {a, b} = useContext(Context);
const derived = computeDerived(a, b);

and it would get converted to something like this:

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.)

@acdlite acdlite force-pushed the context-selectors branch 4 times, most recently from a0bb10a to e99f863 Compare July 8, 2021 13:58
@acdlite acdlite changed the base branch from master to main July 8, 2021 13:59
@acdlite acdlite force-pushed the context-selectors branch 2 times, most recently from fffc35b to 6a215af Compare July 8, 2021 16:58
acdlite added 5 commits July 10, 2021 12:59
This block is getting hard to read so I moved it to a separate function.
I'm about to refactor the logic that wraps around this path.

Ideally this early bailout path would happen before the begin phase
phase. Perhaps during reconcilation of the parent fiber's children.
The only reason we pass `updateLanes` to some begin functions is to
check if we can perform an early bail out. But this is also available
as `current.lanes`, so we can read it from there instead.

I think the only reason we didn't do it this way originally is because
components that have two phases — error and Suspense boundaries —
use `workInProgress.lanes` to prevent a bail out, since during the
initial render there is no `current`. But we can check the `DidCapture`
flag instead, which we use elsewhere to detect the second phase.
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.)
This will to make it easier to A/B test, or to revert if we abandon the
experiment. Using a selector will not change the return type of
`useContext`. Use a userspace hook to get the selected value:

```js
function useContextSelector<C, S>(Context: C, selector: C => S): S {
  const context = useContext(Context, {unstable_selector: selector});
  const selectedContext = selector(context);
  return selectedContext;
}
```
@acdlite acdlite force-pushed the context-selectors branch from 6a215af to c0b8b58 Compare July 10, 2021 17:12
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
CLA Signed React Core Team Opened by a member of the React Core Team
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants