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

useMutableSource hook #18000

Merged
merged 27 commits into from Mar 11, 2020
Merged

useMutableSource hook #18000

merged 27 commits into from Mar 11, 2020

Conversation

bvaughn
Copy link
Contributor

@bvaughn bvaughn commented Feb 7, 2020

useMutableSource() enables React components to safely and efficiently read from a mutable external source in Concurrent Mode. The API will detect mutations that occur during a render to avoid tearing and it will automatically schedule updates when the source is mutated.

RFC: reactjs/rfcs#147

@facebook-github-bot facebook-github-bot added CLA Signed React Core Team Opened by a member of the React Core Team labels Feb 7, 2020
@codesandbox-ci
Copy link

codesandbox-ci bot commented Feb 7, 2020

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 dee1164:

Sandbox Source
happy-clarke-zo2tp Configuration

@sizebot
Copy link

sizebot commented Feb 7, 2020

Details of bundled changes.

Comparing: 30a998d...dee1164

react-dom

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactDOM-profiling.js +1.3% +1.5% 422.63 KB 428.22 KB 76.63 KB 77.79 KB FB_WWW_PROFILING
react-dom-server.browser.development.js +0.3% +0.4% 135.4 KB 135.83 KB 34.82 KB 34.97 KB UMD_DEV
react-dom-server.browser.production.min.js 🔺+0.3% 🔺+0.2% 20.1 KB 20.16 KB 7.44 KB 7.46 KB UMD_PROD
react-dom-unstable-native-dependencies.development.js 0.0% -0.0% 55.92 KB 55.92 KB 14.48 KB 14.47 KB NODE_DEV
react-dom.development.js +1.3% +1.1% 892.49 KB 903.66 KB 196.69 KB 198.95 KB UMD_DEV
react-dom-unstable-native-dependencies.production.min.js 0.0% -0.1% 10.04 KB 10.04 KB 3.37 KB 3.36 KB NODE_PROD
react-dom.production.min.js 🔺+1.3% 🔺+1.5% 117.17 KB 118.67 KB 37.52 KB 38.09 KB UMD_PROD
react-dom.profiling.min.js +1.2% +1.3% 120.88 KB 122.38 KB 38.77 KB 39.29 KB UMD_PROFILING
react-dom.development.js +1.3% +1.1% 849.35 KB 860.02 KB 194.22 KB 196.44 KB NODE_DEV
react-dom-server.node.development.js +0.3% +0.4% 129.68 KB 130.09 KB 34.64 KB 34.77 KB NODE_DEV
react-dom.production.min.js 🔺+1.3% 🔺+1.4% 117.24 KB 118.75 KB 36.79 KB 37.31 KB NODE_PROD
react-dom-server.node.production.min.js 🔺+0.3% 🔺+0.2% 20.43 KB 20.48 KB 7.55 KB 7.57 KB NODE_PROD
react-dom.profiling.min.js +1.2% +1.4% 121.1 KB 122.61 KB 37.98 KB 38.51 KB NODE_PROFILING
ReactDOM-dev.js +1.1% +1.1% 971.01 KB 981.94 KB 215.97 KB 218.25 KB FB_WWW_DEV
ReactDOM-prod.js 🔺+1.3% 🔺+1.5% 405.32 KB 410.57 KB 73.6 KB 74.68 KB FB_WWW_PROD
ReactDOMTesting-dev.js +1.2% +1.1% 908.33 KB 919.25 KB 202.83 KB 205.13 KB FB_WWW_DEV
ReactDOMTesting-prod.js 🔺+1.3% 🔺+1.5% 386.38 KB 391.57 KB 70.34 KB 71.38 KB FB_WWW_PROD
react-dom-server.browser.development.js +0.3% +0.4% 128.46 KB 128.87 KB 34.39 KB 34.52 KB NODE_DEV
ReactDOMTesting-profiling.js +1.3% +1.5% 386.38 KB 391.57 KB 70.34 KB 71.38 KB FB_WWW_PROFILING
react-dom-server.browser.production.min.js 🔺+0.3% 🔺+0.2% 20.02 KB 20.07 KB 7.4 KB 7.42 KB NODE_PROD
react-dom-unstable-fizz.browser.production.min.js 0.0% -0.1% 1.17 KB 1.17 KB 690 B 689 B UMD_PROD
ReactDOMServer-dev.js +0.3% +0.4% 138.6 KB 139.01 KB 35.33 KB 35.48 KB FB_WWW_DEV
react-dom-test-utils.development.js 0.0% -0.0% 49.61 KB 49.61 KB 13.52 KB 13.52 KB NODE_DEV
ReactDOMServer-prod.js 🔺+0.3% 🔺+0.3% 47.57 KB 47.71 KB 11.07 KB 11.11 KB FB_WWW_PROD
react-dom-test-utils.production.min.js 0.0% -0.0% 10.85 KB 10.85 KB 4.1 KB 4.1 KB NODE_PROD
react-dom-unstable-native-dependencies.development.js 0.0% -0.0% 58.97 KB 58.97 KB 14.71 KB 14.71 KB UMD_DEV
react-dom-unstable-fizz.browser.production.min.js 0.0% -0.2% 1013 B 1013 B 602 B 601 B NODE_PROD
react-dom-unstable-native-dependencies.production.min.js 0.0% -0.1% 10.31 KB 10.31 KB 3.49 KB 3.48 KB UMD_PROD

react-native-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactNativeRenderer-dev.js +1.7% +1.6% 638.61 KB 649.46 KB 138.35 KB 140.63 KB RN_OSS_DEV
ReactNativeRenderer-prod.js 🔺+2.0% 🔺+2.3% 261.74 KB 266.95 KB 45.41 KB 46.44 KB RN_OSS_PROD
ReactNativeRenderer-profiling.js +1.9% +2.2% 273.54 KB 278.75 KB 47.66 KB 48.71 KB RN_OSS_PROFILING
ReactFabric-dev.js +1.7% +1.7% 620.56 KB 631.32 KB 134.02 KB 136.28 KB RN_OSS_DEV
ReactFabric-prod.js 🔺+2.1% 🔺+2.4% 253.71 KB 258.94 KB 43.92 KB 44.96 KB RN_OSS_PROD
ReactFabric-profiling.js +2.0% +2.2% 265.51 KB 270.73 KB 46.19 KB 47.2 KB RN_OSS_PROFILING

react-art

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-art.development.js +2.0% +2.0% 525.92 KB 536.47 KB 113.35 KB 115.58 KB NODE_DEV
react-art.production.min.js 🔺+2.2% 🔺+2.5% 70.02 KB 71.53 KB 20.99 KB 21.51 KB NODE_PROD
ReactART-dev.js +1.8% +1.8% 587.24 KB 598.04 KB 122.8 KB 125.07 KB FB_WWW_DEV
ReactART-prod.js 🔺+2.2% 🔺+2.5% 236.83 KB 242.07 KB 40.19 KB 41.19 KB FB_WWW_PROD
react-art.development.js +1.8% +1.7% 621.26 KB 632.3 KB 131.02 KB 133.25 KB UMD_DEV
react-art.production.min.js 🔺+1.4% 🔺+1.6% 104.98 KB 106.48 KB 31.82 KB 32.32 KB UMD_PROD

react-test-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-test-renderer-shallow.development.js 0.0% -0.0% 38.39 KB 38.39 KB 9.29 KB 9.29 KB UMD_DEV
react-test-renderer.development.js +2.0% +2.0% 532.48 KB 543.04 KB 114.84 KB 117.08 KB NODE_DEV
react-test-renderer.production.min.js 🔺+2.1% 🔺+2.5% 71.81 KB 73.32 KB 21.52 KB 22.06 KB NODE_PROD
ReactTestRenderer-dev.js +1.9% +1.9% 559.51 KB 570.3 KB 118.04 KB 120.32 KB FB_WWW_DEV
react-test-renderer.development.js +2.0% +2.0% 558.74 KB 569.78 KB 116.2 KB 118.46 KB UMD_DEV
react-test-renderer.production.min.js 🔺+2.1% 🔺+2.3% 71.99 KB 73.5 KB 21.89 KB 22.39 KB UMD_PROD

react-reconciler

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-reconciler.development.js +2.1% +2.0% 562.07 KB 573.77 KB 118.83 KB 121.19 KB NODE_DEV
react-reconciler.production.min.js 🔺+2.3% 🔺+2.5% 74.47 KB 76.19 KB 21.99 KB 22.55 KB NODE_PROD
react-reconciler-reflection.production.min.js 0.0% -0.1% 2.8 KB 2.8 KB 1.2 KB 1.2 KB NODE_PROD

react

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react.development.js 0.0% -0.0% 100.31 KB 100.31 KB 24.65 KB 24.64 KB UMD_DEV
react.production.min.js 0.0% -0.0% 11.59 KB 11.59 KB 4.56 KB 4.56 KB UMD_PROD
react.profiling.min.js 0.0% -0.0% 15.13 KB 15.13 KB 5.68 KB 5.67 KB UMD_PROFILING
react.development.js 0.0% -0.0% 62.14 KB 62.14 KB 16.92 KB 16.92 KB NODE_DEV
react.production.min.js 0.0% -0.1% 6.53 KB 6.53 KB 2.65 KB 2.65 KB NODE_PROD
React-dev.js +0.8% +0.6% 75.66 KB 76.26 KB 19.16 KB 19.28 KB FB_WWW_DEV
React-prod.js 🔺+2.0% 🔺+1.9% 17.72 KB 18.08 KB 4.58 KB 4.67 KB FB_WWW_PROD
React-profiling.js +2.0% +1.9% 17.72 KB 18.08 KB 4.58 KB 4.67 KB FB_WWW_PROFILING

react-debug-tools

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-debug-tools.development.js +4.6% +2.7% 18.48 KB 19.33 KB 5.17 KB 5.31 KB NODE_DEV
react-debug-tools.production.min.js 🔺+5.9% 🔺+4.4% 5.49 KB 5.81 KB 2.1 KB 2.2 KB NODE_PROD

ReactDOM: size: 🔺+0.3%, gzip: 🔺+0.2%

React: size: 0.0%, gzip: -0.0%

Size changes (stable)

Generated by 🚫 dangerJS against dee1164

@sizebot
Copy link

sizebot commented Feb 7, 2020

Details of bundled changes.

Comparing: 30a998d...dee1164

react

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react.development.js +0.6% +0.6% 108.33 KB 108.97 KB 25.89 KB 26.03 KB UMD_DEV
react.production.min.js 🔺+1.7% 🔺+1.3% 12.45 KB 12.66 KB 4.81 KB 4.87 KB UMD_PROD
react.profiling.min.js +1.3% +0.9% 15.99 KB 16.2 KB 5.89 KB 5.95 KB UMD_PROFILING
react.development.js +0.9% +0.7% 69.79 KB 70.39 KB 18.15 KB 18.27 KB NODE_DEV
react.production.min.js 🔺+2.9% 🔺+2.2% 7.51 KB 7.73 KB 2.91 KB 2.98 KB NODE_PROD
React-dev.js +0.8% +0.7% 74.98 KB 75.59 KB 18.99 KB 19.11 KB FB_WWW_DEV
React-prod.js 🔺+2.0% 🔺+2.0% 17.66 KB 18.02 KB 4.57 KB 4.66 KB FB_WWW_PROD
React-profiling.js +2.0% +2.0% 17.66 KB 18.02 KB 4.57 KB 4.66 KB FB_WWW_PROFILING

react-dom

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-dom.profiling.min.js +1.2% +1.5% 125.48 KB 127 KB 39.11 KB 39.68 KB NODE_PROFILING
react-dom-unstable-native-dependencies.development.js 0.0% -0.0% 58.98 KB 58.98 KB 14.71 KB 14.71 KB UMD_DEV
ReactDOM-dev.js +1.2% +1.1% 925.09 KB 936.02 KB 206.08 KB 208.39 KB FB_WWW_DEV
react-dom-unstable-native-dependencies.production.min.js 0.0% -0.1% 10.32 KB 10.32 KB 3.5 KB 3.49 KB UMD_PROD
ReactDOMServer-dev.js +0.3% +0.4% 137.75 KB 138.16 KB 35.19 KB 35.34 KB FB_WWW_DEV
react-dom-test-utils.production.min.js 0.0% 0.0% 11.01 KB 11.01 KB 4.17 KB 4.17 KB UMD_PROD
ReactDOMTesting-dev.js +1.2% +1.2% 881.68 KB 892.61 KB 197.41 KB 199.71 KB FB_WWW_DEV
ReactDOMTesting-prod.js 🔺+1.4% 🔺+1.5% 372.82 KB 378.01 KB 68.14 KB 69.19 KB FB_WWW_PROD
react-dom-test-utils.development.js 0.0% -0.0% 49.63 KB 49.63 KB 13.53 KB 13.53 KB NODE_DEV
ReactDOMTesting-profiling.js +1.4% +1.5% 372.82 KB 378.01 KB 68.14 KB 69.19 KB FB_WWW_PROFILING
react-dom-server.node.development.js +0.3% +0.4% 131.19 KB 131.6 KB 34.86 KB 34.99 KB NODE_DEV
react-dom-test-utils.production.min.js 0.0% 0.0% 10.86 KB 10.86 KB 4.1 KB 4.11 KB NODE_PROD
react-dom-server.node.production.min.js 🔺+0.3% 🔺+0.2% 20.88 KB 20.94 KB 7.63 KB 7.65 KB NODE_PROD
react-dom.development.js +1.2% +1.1% 922.96 KB 934.14 KB 201.97 KB 204.24 KB UMD_DEV
react-dom-server.browser.development.js +0.3% +0.4% 136.99 KB 137.42 KB 35.02 KB 35.17 KB UMD_DEV
react-dom.production.min.js 🔺+1.2% 🔺+1.5% 121.36 KB 122.87 KB 38.67 KB 39.23 KB UMD_PROD
react-dom-server.browser.production.min.js 🔺+0.3% 🔺+0.2% 20.56 KB 20.62 KB 7.52 KB 7.54 KB UMD_PROD
react-dom.profiling.min.js +1.2% +1.4% 125.19 KB 126.7 KB 39.91 KB 40.49 KB UMD_PROFILING
react-dom.development.js +1.2% +1.1% 878.54 KB 889.23 KB 199.45 KB 201.69 KB NODE_DEV
react-dom-server.browser.development.js +0.3% +0.4% 129.97 KB 130.38 KB 34.61 KB 34.74 KB NODE_DEV
react-dom.production.min.js 🔺+1.2% 🔺+1.5% 121.51 KB 123.02 KB 37.87 KB 38.42 KB NODE_PROD
react-dom-server.browser.production.min.js 🔺+0.3% 🔺+0.3% 20.48 KB 20.53 KB 7.48 KB 7.5 KB NODE_PROD
ReactDOM-prod.js 🔺+1.4% 🔺+1.5% 378.71 KB 383.96 KB 68.83 KB 69.9 KB FB_WWW_PROD
ReactDOMServer-prod.js 🔺+0.3% 🔺+0.4% 46.86 KB 47.01 KB 10.89 KB 10.93 KB FB_WWW_PROD
ReactDOM-profiling.js +1.4% +1.6% 395.97 KB 401.55 KB 71.81 KB 72.92 KB FB_WWW_PROFILING
react-dom-unstable-native-dependencies.development.js 0.0% -0.0% 55.94 KB 55.94 KB 14.48 KB 14.48 KB NODE_DEV
react-dom-unstable-native-dependencies.production.min.js 0.0% -0.1% 10.05 KB 10.05 KB 3.38 KB 3.37 KB NODE_PROD
react-dom-unstable-fizz.browser.production.min.js 0.0% -0.2% 1 KB 1 KB 610 B 609 B NODE_PROD

react-native-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactFabric-prod.js 🔺+2.1% 🔺+2.4% 253.72 KB 258.95 KB 43.93 KB 44.97 KB RN_OSS_PROD
ReactFabric-profiling.js +2.0% +2.2% 265.52 KB 270.75 KB 46.2 KB 47.21 KB RN_OSS_PROFILING
ReactNativeRenderer-prod.js 🔺+2.0% 🔺+2.3% 261.76 KB 266.96 KB 45.42 KB 46.45 KB RN_OSS_PROD
ReactNativeRenderer-profiling.js +1.9% +2.2% 273.55 KB 278.76 KB 47.67 KB 48.72 KB RN_OSS_PROFILING
ReactNativeRenderer-dev.js +1.7% +1.6% 641.33 KB 652.18 KB 138.7 KB 140.98 KB RN_FB_DEV
ReactNativeRenderer-prod.js 🔺+2.0% 🔺+2.3% 261.9 KB 267.11 KB 45.45 KB 46.48 KB RN_FB_PROD
ReactNativeRenderer-profiling.js +1.9% +2.2% 273.7 KB 278.9 KB 47.7 KB 48.74 KB RN_FB_PROFILING
ReactFabric-dev.js +1.7% +1.7% 620.58 KB 631.33 KB 134.03 KB 136.28 KB RN_OSS_DEV
ReactFabric-dev.js +1.7% +1.7% 623.29 KB 634.04 KB 134.35 KB 136.61 KB RN_FB_DEV
ReactFabric-prod.js 🔺+2.1% 🔺+2.4% 253.87 KB 259.1 KB 43.96 KB 45 KB RN_FB_PROD
ReactNativeRenderer-dev.js +1.7% +1.6% 638.63 KB 649.47 KB 138.36 KB 140.64 KB RN_OSS_DEV
ReactFabric-profiling.js +2.0% +2.2% 265.67 KB 270.89 KB 46.23 KB 47.24 KB RN_FB_PROFILING

react-art

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-art.development.js +1.7% +1.7% 642.22 KB 653.26 KB 134.71 KB 136.93 KB UMD_DEV
react-art.production.min.js 🔺+1.4% 🔺+1.6% 107.73 KB 109.24 KB 32.57 KB 33.09 KB UMD_PROD
react-art.development.js +1.9% +1.9% 546.03 KB 556.6 KB 117.08 KB 119.32 KB NODE_DEV
react-art.production.min.js 🔺+2.1% 🔺+2.5% 72.71 KB 74.23 KB 21.7 KB 22.24 KB NODE_PROD
ReactART-dev.js +1.9% +1.9% 559.17 KB 569.97 KB 117.15 KB 119.42 KB FB_WWW_DEV
ReactART-prod.js 🔺+2.3% 🔺+2.7% 227.98 KB 233.22 KB 38.76 KB 39.8 KB FB_WWW_PROD

react-test-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-test-renderer-shallow.development.js 0.0% -0.0% 38.4 KB 38.4 KB 9.3 KB 9.3 KB UMD_DEV
react-test-renderer-shallow.production.min.js 0.0% 0.0% 12.92 KB 12.92 KB 3.94 KB 3.94 KB UMD_PROD
ReactTestRenderer-dev.js +1.9% +1.9% 559.52 KB 570.31 KB 118.05 KB 120.32 KB FB_WWW_DEV
react-test-renderer.development.js +2.0% +2.0% 558.76 KB 569.8 KB 116.21 KB 118.48 KB UMD_DEV
react-test-renderer.production.min.js 🔺+2.1% 🔺+2.3% 72.02 KB 73.53 KB 21.91 KB 22.41 KB UMD_PROD
react-test-renderer.development.js +2.0% +2.0% 532.5 KB 543.06 KB 114.85 KB 117.09 KB NODE_DEV
react-test-renderer.production.min.js 🔺+2.1% 🔺+2.5% 71.83 KB 73.34 KB 21.54 KB 22.07 KB NODE_PROD

react-reconciler

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-reconciler.development.js +2.0% +1.9% 584.51 KB 596.22 KB 123.03 KB 125.42 KB NODE_DEV
react-reconciler.production.min.js 🔺+2.2% 🔺+2.4% 77.57 KB 79.28 KB 22.75 KB 23.3 KB NODE_PROD
react-reconciler-reflection.production.min.js 0.0% -0.1% 2.81 KB 2.81 KB 1.21 KB 1.21 KB NODE_PROD

react-debug-tools

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-debug-tools.development.js +4.6% +2.7% 18.49 KB 19.34 KB 5.18 KB 5.32 KB NODE_DEV
react-debug-tools.production.min.js 🔺+5.8% 🔺+4.3% 5.5 KB 5.82 KB 2.11 KB 2.21 KB NODE_PROD

ReactDOM: size: 0.0%, gzip: -0.1%

React: size: 🔺+1.7%, gzip: 🔺+1.3%

Size changes (experimental)

Generated by 🚫 dangerJS against dee1164

fiber: Fiber,
expirationTime: ExpirationTime,
) {
function scheduleUpdateOnFiber(fiber: Fiber, expirationTime: ExpirationTime) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This export was never referenced (only the scheduleWork alias below) so I removed it.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Do you mind you removing scheduleWork instead? 😆 I meant to rename all the callsites to the more specific name during the last refactor.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure.

@bvaughn
Copy link
Contributor Author

bvaughn commented Feb 13, 2020

This PR is ready for review. I've updated it to reflect the latest thinking about the API.

@bvaughn bvaughn changed the title useMutableSource hook [work in progress] useMutableSource hook Feb 13, 2020
@bvaughn
Copy link
Contributor Author

bvaughn commented Feb 13, 2020

I do not understand the Circle CI lint_build error. I'm tempted to say it's a CI problem and not related to this PR though.


isSafeToReadFromSource =
pendingExpirationTime === NoWork ||
pendingExpirationTime >= expirationTime;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note to self: If this component is newly mounting, we need to always check the version number as well, to account for the case of tearing between newly mounted components that have not yet subscribed to the store (and so may miss a tear).

I'll add a test for this (and update the code) tomorrow.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Related: if the source version matches the work-in-progress version, then you don't need to check if the current render is inclusive of the pending mutation updates. Because that check would only fail if there was a mutation since the work-in-progress version was originally set, in which case the the version would have been bumped.

In other words, you only have to check for pending mutation updates if there's not already a work-in-progress version, i.e. if this is the first mutable read of this source during this render. For subsequent reads, the version alone is sufficient to tell if there was a mutation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, this sounds right. I could avoid checking the expiration time if I already have a WIP version. Good suggestion.

Copy link
Collaborator

@acdlite acdlite left a comment

Choose a reason for hiding this comment

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

Great work! (Sorry for the avalanche of comments! Because the high level approach is essentially correct, my feedback is more detailed than usual for a first review pass.)

Some of my feedback is a bit nitpicky, but I really just have one big note: we should fork the mount and update implementations. They have different trade offs. (By "mount" I mean both new useMutableSource hooks and when the source or config changes, i.e. "mount" in the useEffect sense of the word.)

For renders that don't include a new hook, we can support full concurrency with no de-opts.

To illustrate what I mean, here is an additional test case to consider — multiple pending mutations at different priorities, no new mounts:

  • Render a tree that includes multiple useMutableSource hooks.
  • Mutate one or more of the sources at high priority.
  • Before processing the update, mutate again at a lower priority.
  • Now flush the work.
  • There should be two commits: the high priority mutation, followed by the low priority mutation.

The way you would implement this is with a state queue. You can use the one provided by useState/useReducer. This works because for mutations after mount, we can call getSnapshot in the event that triggers the mutation instead of in the render phase. Then we add that snapshot to the queue. At that point, they're exactly like normal updates. If the source is mutated before the update commits, that's fine because we already took the snapshot. If there are multiple snapshots, that's also fine because each one of them is in the queue.

It's only in the mount case where we're forced to read during the render phase, which necessitates the extra consistency checks.

packages/react-reconciler/src/ReactFiberHooks.js Outdated Show resolved Hide resolved
packages/shared/ReactMutableSource.js Outdated Show resolved Hide resolved
fiber: Fiber,
expirationTime: ExpirationTime,
) {
function scheduleUpdateOnFiber(fiber: Fiber, expirationTime: ExpirationTime) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do you mind you removing scheduleWork instead? 😆 I meant to rename all the callsites to the more specific name during the last refactor.

packages/shared/ReactMutableSource.js Outdated Show resolved Hide resolved
packages/react-reconciler/src/ReactFiberWorkLoop.js Outdated Show resolved Hide resolved
packages/react-reconciler/src/ReactFiberHooks.js Outdated Show resolved Hide resolved
packages/react-reconciler/src/ReactFiberCommitWork.js Outdated Show resolved Hide resolved

hook.memoizedState = memoizedState;

if (prevSource !== source) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

What if the config changes?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Interesting question. My thinking had been that a change in config would really just be a change in getSnapshot dependencies, and not a change in subscribe. Sebastian's suggestion on the RFC to get rid of the wrapper config object and pass both callbacks directly would make this more explicit, in which case I will add a check for that here as well.

isPrimaryRenderer: boolean,
): void {
if (isPrimaryRenderer) {
mutableSource._workInProgressVersionPrimary = version;
Copy link
Collaborator

Choose a reason for hiding this comment

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

In the context implementation, we fire a warning if we detect multiple primary renderers.

if (__DEV__) {
if (
context._currentRenderer !== undefined &&
context._currentRenderer !== null &&
context._currentRenderer !== rendererSigil
) {
console.error(
'Detected multiple renderers concurrently rendering the ' +
'same context provider. This is currently unsupported.',
);
}
context._currentRenderer = rendererSigil;

We should probably do the same here. Although I think it will need to go in getWorkInProgressVersion instead of set.

);

// If an update is already scheduled for this source, re-use the same priority.
if (alreadyScheduledExpirationTime !== undefined) {
Copy link
Collaborator

@acdlite acdlite Feb 14, 2020

Choose a reason for hiding this comment

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

This check means that if you update at one priority, then again at a different priority, the first priority always wins. We should support multiple concurrent priorities, which is solved by using the useState queue. (The test case I described in my other comment would cover this.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, my thinking on this was that scheduling more than one update is only valid if we eagerly read snapshot values. I think the key question is whether or not that's a positive change. If it is, then going with an update queue makes sense.

@acdlite
Copy link
Collaborator

acdlite commented Feb 14, 2020

The lint build error is legit. I looked at the ReactFabric-dev artifact and found this:

// ReactFabric-dev.js:11656
useMutableSource: function(source, config) {
  currentHookNameInDev = "useMutableSource";
  mountHookTypesDev();
  return Snapshot > config;
}

Looks like some sort of build error related to Flow types?

Phew that we have the lint build task! Also an argument for why we should be running more of our tests against the bundles (and therefore moving away from .internal test files).

@bvaughn
Copy link
Contributor Author

bvaughn commented Feb 14, 2020

The lint build error is legit.

That's interesting! 😄 I initially assumed it was a transient issue yesterday. Just hadn't had the chance to dig into it yet today.

Brian Vaughn added 12 commits March 10, 2020 11:30
This prevents the subscription callback from closing over stale values.
Even if the current snapshot value is the same, there may be pending, lower priority updates that we no longer want to eventually render.
I initially thought that we could treat them as safe if the returned snapshot value was the same, but this ignored the case where the underlying source was mutated between when the state update was scheduled and when the component later rendered with a new getSnapshot function. (This commit includes a test that failed without this fix.)
1. Reset mutable source WIP version in complete and unwind work (rather than prepare fresh stack).
2. Add check to warn about multiple primary/secondary renderers using the same mutable source. (Patterned after our current context warning.)
3. Changed the way I'm calculating expiration time for mutated sources to use computeExpirationForFiber().
4. Changed undefined unsubscribe function from invariant to warning.
5. Replaced !== checks with Object.is() checks to be more consistent with precedent.
6. Added a couple of new warning tests. (One of them has a pending TODO.)
1. Use currentlyRenderingFiber to calculate expiration time rather than using a ref.
2. Use polyfilled is() rather than Object.is()
3. Add __EXPERIMENTAL__ guard to test since new APIs aren't in stable build
4. Removed error code that was changed to warning.
@bvaughn
Copy link
Contributor Author

bvaughn commented Mar 10, 2020

I rebased my PR on top of #18265 to see if it fixes the failing test I added recently. It does! 👍

I realized that there's another multi-renderer case though that we weren't handling properly: when a source is used by one renderer, then mutated, then used by another renderer. To be clear, this isn't supported, but it currently throws a user visible error.

That's because when a change of version is detected, React throws and unwinds- and is supposed to reset all WIP version numbers. This is done by looping through the mutable sources that were updated during the current render and resetting them. In this case though, we never updated the torn source for the second renderer (only read from it) so React doesn't know to reset its version while unwinding. (This means the re-render attempt will encounter the same error, and React will give up.)

I think we have a couple of options here:

  1. Special case the mutable source we are about to throw for by flagging it for reset explicitly. (This kind of sucks but I think it may be the best option to handle the edge case.)
  2. Track reads in addition to writes so we'll be sure to get the torn source too. This would add overhead to the hotter read path though so it's probably not a great idea.
  3. Be more aggressive about the multi-renderer case and don't even try to support it. (Basically replace the warning with an invariant.)
  4. Something else I haven't thought of?

Edit for clarity None of the above "solutions" are great. Even the one I chose to go with for now can lead to tearing in the first renderer when it resumes. I think it's the best we can do though, if we want to avoid the invariant.

Brian Vaughn added 2 commits March 10, 2020 12:26
If a mutation happens between the first and second renderer to use a source, we need to explicitly reset the source before restarting the second renderer.
I am being a little lazy here and merging instead of rebasing because practically every commit conflicted with Dominic's recent useEvent PR. If anyone feel strongly about this, I will revert the commit and rebase. Since the commit will be squashes away during the merge though, I don't think it matters much at this point.
@acdlite
Copy link
Collaborator

acdlite commented Mar 11, 2020

I think this is fine. We are intentionally not supporting anything more than a primary renderer and a secondary renderer, to keep the simpler implementation. And the reason to use a warning instead of an error is so the check is stripped out in prod.

@bvaughn
Copy link
Contributor Author

bvaughn commented Mar 11, 2020

Right. That was my thinking too.

Okay then. Good to go?

@Sawtaytoes
Copy link

Is there gonna be any documentation about how to use this hook on the React docs site?

@bvaughn
Copy link
Contributor Author

bvaughn commented Mar 9, 2021

Once it's in a stable release, yes. This is still only available in the experimental React release channel.

@gaearon gaearon mentioned this pull request Mar 29, 2022
kodiakhq bot pushed a commit to vercel/next.js that referenced this pull request May 8, 2022
- [x] Make sure the linting passes by running `yarn lint`

Back in 2019, React released the first version of `use-subscription` (facebook/react#15022). At the time, we only has limited information about concurrent rendering, and #9026 add the initial concurrent mode support.

In 2020, React provides a first-party official API `useMutableSource` (reactjs/rfcs#147, facebook/react#18000):

> ... enables React components to safely and efficiently read from a mutable external source in Concurrent Mode.

React 18 introduces `useMutableSource`'s replacement `useSyncExternalStore` (see details here: reactwg/react-18#86), and React changes `use-subscription` implementation to use `useSyncExternalStore` directly: facebook/react#24289

> In React 18, `React.useSyncExternalStore` is a built-in replacement for `useSubscription`.
> 
> This PR makes `useSubscription` simply use `React.useSyncExternalStore` when available. For pre-18, it uses a `use-sync-external-store` shim which is very similar in `use-subscription` but fixes some flaws with concurrent rendering.

And according to `use-subscription`:

> You may now migrate to [`use-sync-external-store`](https://www.npmjs.com/package/use-sync-external-store) directly instead, which has the same API as `React.useSyncExternalStore`. The `use-subscription` package is now a thin wrapper over `use-sync-external-store` and will not be updated further.

The PR does exactly that:

- Removes the precompiled `use-subscription` introduced in #35746
- Adds the `use-sync-external-store` to the dependencies.
  - The `use-sync-external-store` package enables compatibility with React 16 and React 17.
  - Do not pre-compile `use-sync-external-store` since it is also the dependency of some popular React state management libraries like `react-redux`, `zustand`, `valtio`, `@xstate/react` and `@apollo/client`, etc. By install
- Replace `useSubscription` usage with `useSyncExternalStore` 

---

Ref: #9026, #35746 and #36159


Co-authored-by: Jiachi Liu <4800338+huozhi@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
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.

None yet