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

Point useSubscription to useSyncExternalStore shim #24289

Merged
merged 4 commits into from
Apr 11, 2022

Conversation

gaearon
Copy link
Collaborator

@gaearon gaearon commented Apr 7, 2022

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.

@facebook-github-bot facebook-github-bot added CLA Signed React Core Team Opened by a member of the React Core Team labels Apr 7, 2022
@sizebot
Copy link

sizebot commented Apr 7, 2022

Comparing: 4bc465a...41c3664

Critical size changes

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

Name +/- Base Current +/- gzip Base gzip Current gzip
oss-stable/react-dom/cjs/react-dom.production.min.js = 131.40 kB 131.40 kB = 41.98 kB 41.98 kB
oss-experimental/react-dom/cjs/react-dom.production.min.js = 136.45 kB 136.45 kB = 43.43 kB 43.43 kB
facebook-www/ReactDOM-prod.classic.js = 433.07 kB 433.07 kB = 79.60 kB 79.60 kB
facebook-www/ReactDOM-prod.modern.js = 418.07 kB 418.07 kB = 77.24 kB 77.24 kB
facebook-www/ReactDOMForked-prod.classic.js = 433.07 kB 433.07 kB = 79.61 kB 79.61 kB
oss-experimental/use-subscription/cjs/use-subscription.production.min.js = 0.80 kB 0.41 kB = 0.44 kB 0.29 kB
oss-stable-semver/use-subscription/cjs/use-subscription.production.min.js = 0.80 kB 0.41 kB = 0.44 kB 0.29 kB
oss-stable/use-subscription/cjs/use-subscription.production.min.js = 0.80 kB 0.41 kB = 0.44 kB 0.29 kB
oss-experimental/use-subscription/cjs/use-subscription.development.js = 5.61 kB 1.50 kB = 2.08 kB 0.66 kB
oss-stable-semver/use-subscription/cjs/use-subscription.development.js = 5.61 kB 1.50 kB = 2.08 kB 0.66 kB
oss-stable/use-subscription/cjs/use-subscription.development.js = 5.61 kB 1.50 kB = 2.08 kB 0.66 kB

Significant size changes

Includes any change greater than 0.2%:

Expand to show
Name +/- Base Current +/- gzip Base gzip Current gzip
oss-experimental/use-subscription/cjs/use-subscription.production.min.js = 0.80 kB 0.41 kB = 0.44 kB 0.29 kB
oss-stable-semver/use-subscription/cjs/use-subscription.production.min.js = 0.80 kB 0.41 kB = 0.44 kB 0.29 kB
oss-stable/use-subscription/cjs/use-subscription.production.min.js = 0.80 kB 0.41 kB = 0.44 kB 0.29 kB
oss-experimental/use-subscription/cjs/use-subscription.development.js = 5.61 kB 1.50 kB = 2.08 kB 0.66 kB
oss-stable-semver/use-subscription/cjs/use-subscription.development.js = 5.61 kB 1.50 kB = 2.08 kB 0.66 kB
oss-stable/use-subscription/cjs/use-subscription.development.js = 5.61 kB 1.50 kB = 2.08 kB 0.66 kB

Generated by 🚫 dangerJS against 41c3664

@rickhanlonii
Copy link
Member

rickhanlonii commented Apr 8, 2022

@gaearon the failures are because useSubscription schedules updates from mutations inside startTransition as transitions, but useSyncExternalStore forces a sync render even when inside startTransition.

Specifically, useSubscription schedules the update here (so if startTransition is anywhere on the stack, this update will be a transition):

setState(prevState => {
// Ignore values from stale sources!
// Since we subscribe an unsubscribe in a passive effect,
// it's possible that this callback will be invoked for a stale (previous) subscription.
// This check avoids scheduling an update for that stale subscription.
if (
prevState.getCurrentValue !== getCurrentValue ||
prevState.subscribe !== subscribe
) {
return prevState;
}
// Some subscriptions will auto-invoke the handler, even if the value hasn't changed.
// If the value hasn't changed, no update is needed.
// Return state as-is so React can bail out and avoid an unnecessary render.
if (prevState.value === value) {
return prevState;
}
return {...prevState, value};
});

And useSyncExternalStore schedules it here:

if (checkIfSnapshotChanged(inst)) {
// Force a re-render.
forceStoreRerender(fiber);
}

Which schedules a sync lane update here (instead of calling ensureRootIsScheduled which would check the priority):

function forceStoreRerender(fiber) {
scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp);
}

For the first test, the sync update interrupts the in progress transition and forces two sync updates. That's why you see that grandchild isn't rendered (I think, I'm not 100% sure why grandchild isn't rendered there), and once fixed you'll see two sync renders.

For the second test, since the mutation forces a sync render, toFlushAndYieldThrough is going to show both of the updates because there's no yield between.

@gaearon
Copy link
Collaborator Author

gaearon commented Apr 8, 2022

So arguably this can be considered a bugfix and we can change the test? I want to understand if we can release this in a minor.

@rickhanlonii
Copy link
Member

Yes, I think that's fair. The test don't fail because of broken behavior, but different semantics and the new semantics are better.

@gaearon gaearon merged commit 4997515 into facebook:main Apr 11, 2022
rickhanlonii pushed a commit that referenced this pull request Apr 13, 2022
* Point useSubscription to useSyncExternalStore shim

* Update tests

* Update README

* Ad hoc case
rickhanlonii pushed a commit that referenced this pull request Apr 14, 2022
* Point useSubscription to useSyncExternalStore shim

* Update tests

* Update README

* Ad hoc case
zhengjitf pushed a commit to zhengjitf/react that referenced this pull request Apr 15, 2022
* Point useSubscription to useSyncExternalStore shim

* Update tests

* Update README

* Ad hoc case
rickhanlonii pushed a commit that referenced this pull request Apr 19, 2022
* Point useSubscription to useSyncExternalStore shim

* Update tests

* Update README

* Ad hoc case
facebook-github-bot pushed a commit to facebook/react-native that referenced this pull request Apr 26, 2022
Summary:
This sync includes the following changes:
- **[bd4784c8f](facebook/react@bd4784c8f )**: Revert #24236 (Don't recreate the same fallback on the client if hydrating suspends) ([#24434](facebook/react#24434)) //<dan>//
- **[6d3b6d0f4](facebook/react@6d3b6d0f4 )**: forwardRef et al shouldn't affect if props reused ([#24421](facebook/react#24421)) //<Andrew Clark>//
- **[bd0813766](facebook/react@bd0813766 )**: Fix: useDeferredValue should reuse previous value ([#24413](facebook/react#24413)) //<Andrew Clark>//
- **[9ae80d6a2](facebook/react@9ae80d6a2 )**: Suppress hydration warnings when a preceding sibling suspends ([#24404](facebook/react#24404)) //<Josh Story>//
- **[0dc4e6663](facebook/react@0dc4e6663 )**: Land enableClientRenderFallbackOnHydrationMismatch ([#24410](facebook/react#24410)) //<Andrew Clark>//
- **[354772952](facebook/react@354772952 )**: Land enableSelectiveHydration flag ([#24406](facebook/react#24406)) //<Andrew Clark>//
- **[392808a1f](facebook/react@392808a1f )**: Land enableClientRenderFallbackOnTextMismatch flag ([#24405](facebook/react#24405)) //<Andrew Clark>//
- **[1e748b452](facebook/react@1e748b452 )**: Land enableLazyElements flag ([#24407](facebook/react#24407)) //<Andrew Clark>//
- **[4175f0593](facebook/react@4175f0593 )**: Temporarily feature flag numeric fallback for symbols ([#24401](facebook/react#24401)) //<Ricky>//
- **[a6d53f346](facebook/react@a6d53f346 )**: Revert "Clean up Selective Hydration / Event Replay flag ([#24156](facebook/react#24156))" ([#24402](facebook/react#24402)) //<Ricky>//
- **[ab9cdd34f](facebook/react@ab9cdd34f )**: Bugfix: In legacy mode, call suspended tree's unmount effects when it is deleted ([#24400](facebook/react#24400)) //<Andrew Clark>//
- **[168da8d55](facebook/react@168da8d55 )**: Fix typo that happened during rebasing //<Andrew Clark>//
- **[8bc527a4c](facebook/react@8bc527a4c )**: Bugfix: Fix race condition between interleaved and non-interleaved updates ([#24353](facebook/react#24353)) //<Andrew Clark>//
- **[f7cf077cc](facebook/react@f7cf077cc )**: [Transition Tracing] Add Offscreen Queue ([#24341](facebook/react#24341)) //<Luna Ruan>//
- **[4fc394bbe](facebook/react@4fc394bbe )**: Fix suspense fallback throttling ([#24253](facebook/react#24253)) //<sunderls>//
- **[80170a068](facebook/react@80170a068 )**: Match bundle.name and match upper case entry points ([#24346](facebook/react#24346)) //<Sebastian Markbåge>//
- **[fea6f8da6](facebook/react@fea6f8da6 )**: [Transition Tracing] Add transition to OffscreenState and pendingSuspenseBoundaries to RootState ([#24340](facebook/react#24340)) //<Luna Ruan>//
- **[8e2f9b086](facebook/react@8e2f9b086 )**: move passive flag ([#24339](facebook/react#24339)) //<Luna Ruan>//
- **[55a21ef7e](facebook/react@55a21ef7e )**: fix pushTransition for transition tracing ([#24338](facebook/react#24338)) //<Luna Ruan>//
- **[069d23bb7](facebook/react@069d23bb7 )**:  [eslint-plugin-exhaustive-deps] Fix exhaustive deps check for unstable vars ([#24343](facebook/react#24343)) //<Afzal Sayed>//
- **[4997515b9](facebook/react@4997515b9 )**: Point useSubscription to useSyncExternalStore shim ([#24289](facebook/react#24289)) //<dan>//
- **[01e2bff1d](facebook/react@01e2bff1d )**: Remove unnecessary check ([#24332](facebook/react#24332)) //<zhoulixiang>//
- **[d9a0f9e20](facebook/react@d9a0f9e20 )**: Delete create-subscription folder ([#24288](facebook/react#24288)) //<dan>//
- **[f993ffc51](facebook/react@f993ffc51 )**: Fix infinite update loop that happens when an unmemoized value is passed to useDeferredValue ([#24247](facebook/react#24247)) //<Andrew Clark>//
- **[fa5800226](facebook/react@fa5800226 )**: [Fizz] Pipeable Stream Perf ([#24291](facebook/react#24291)) //<Josh Story>//
- **[0568c0f8c](facebook/react@0568c0f8c )**: Replace zero with NoLanes for consistency in FiberLane ([#24327](facebook/react#24327)) //<Leo>//
- **[e0160d50c](facebook/react@e0160d50c )**: add transition tracing transitions stack ([#24321](facebook/react#24321)) //<Luna Ruan>//
- **[b0f13e5d3](facebook/react@b0f13e5d3 )**: add pendingPassiveTransitions ([#24320](facebook/react#24320)) //<Luna Ruan>//

Changelog:
[General][Changed] - React Native sync for revisions 60e63b9...bd4784c

jest_e2e[run_all_tests]

Reviewed By: kacieb

Differential Revision: D35899012

fbshipit-source-id: 86a885e336fca9f0efa80cd2b8ca040f2cb53853
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>
This was referenced Nov 8, 2024
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.

5 participants