Skip to content

React 18: hydration mismatch when an external store is updated in an effect #22361

Closed
@eps1lon

Description

@eps1lon

React version: #22347

Steps To Reproduce

  1. Read store (using useSyncExternalStore) inside a Suspense boundary where no component suspended
  2. Read store (using useSyncExternalStore) outside of any Suspense boundary
  3. Update store outside of any Suspense boundary in an effect (useEffect)
<Store>
  <Suspense fallback={null}>
    <Demo>inside suspense has hydration mismatch</Demo>
    {/* When the fallback is actually used on the server, we don't get a hydration mismatch */}
    {/* <Suspender /> */}
  </Suspense>
  <Demo>outside suspense has no hydration mismatch</Demo>
  <UpdatesStore />
</Store>
);

Link to code example: https://codesandbox.io/s/react-18-updating-store-in-an-effect-during-mount-causes-hydration-mismatch-uses-m6lwm?file=/src/index.js

The current behavior

<Demo /> inside the Suspense boundary causes a hydration mismatch since it's hydrated with the value set during useEffect.

The expected behavior

No hydration mismatch.

Repro explainer

The repro is based on reduxjs/react-redux#1794 which is based on a usage from the mui.com docs.
The behavior this repro is implementing is reading a value from window.localStorage (e.g. settings) with a fallback on the server.

The store is a Redux store that is the same on Server and Client.
Reading from the store is implemented like so:

const ReduxStoreContext = createContext();

function useValueRedux() {
  const store = useContext(ReduxStoreContext);
  const selector = useCallback(() => store.getState().codeVariant, [store]);

  // The store is equivalent on Client and Server so we can just re-use `getSnapshot` for `getServerSnapshot`
  return useSyncExternalStore(store.subscribe, selector, selector);
}

The repro contains an implementation that uses React Context for the store which works as expected i.e. no hydration mismatches.

Context

I recently stumbled over

(In general, updates inside a passive effect are not encouraged.)

-- #22277

which makes it sound like this behavior is expected because the update is inside a passive effect. Though it's unclear what is meant by "not encourage". How would I render a default value on the Server and populate it with the actually desired value on the Client?

Metadata

Metadata

Assignees

No one assigned

    Labels

    React 18Bug reports, questions, and general feedback about React 18Type: Discussion

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions