Skip to content
New issue

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

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

Already on GitHub? # 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
# for free to join this conversation on GitHub. Already have an account? # 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