Skip to content

Bug: [StrictMode] getSnapshot is not called after subscribing to external store when used in Suspense #35550

Description

@jzhan-canva

in useSyncExternalStore, subscribe is called as a passive effect, so it's possible the store value has changed between render and effect. As a solution, I can see react calls getSnapshot right after subscribing to store, to check if value is stale.

However, when using Suspense in StrictMode, getSnapshot is not called after subscribing to store

React version: 19.2.3

Steps To Reproduce

Consider below code (codesandbox)

const subscribe = (onStoreChange: () => void) => {
  console.log("subscribe called");
  return () => {
    console.log("unsubscribe called");
  };
};
const getSnapshost = () => {
  console.log("getSnapshot called");
  return 1;
};

const Content = ({ promise }: { promise: Promise<any> }) => {
  React.use(promise);
  const value = React.useSyncExternalStore(subscribe, getSnapshost);
  console.log("rendered with value", value);
  return <div>Value {value}</div>;
};

export default function App() {
  const [, reRender] = React.useReducer((state: number) => state + 1, 0);
  const promise = React.useRef(Promise.resolve());
  return (
    <div>
      <button
        onClick={() => {
          console.log("button clicked");
          promise.current = new Promise<void>((resolve) => {
            setTimeout(() => {
              resolve();
            }, 500);
          });
          reRender();
        }}
      >
        Trigger Suspense
      </button>

      <Suspense fallback={<p>Loading...</p>}>
        <Content promise={promise.current} />
      </Suspense>
    </div>
  );
}

The current behavior

In StrictMode, after I click the button to trigger suspense, when it's resolved, it will re-mount the Content component, unsubscribe store, subscribe store, without calling getSnapshot again
my console is

button clicked
getSnapshot called
getSnapshot called
rendered with value 1
getSnapshot called
getSnapshot called
rendered with value 1
unsubscribe called
subscribe called
Image

The expected behavior

The expected behavior is that getSnapshot should be always called after subscribe is called, i.e. in console I should see

subscribe called
getSnapshot called <-- after last subscribe

Activity

  1. Ranamalsingh12 commented on Jan 20, 2026

    @Ranamalsingh12

    Thanks for confirming.

    This behavior seems to break the core safety guarantee of useSyncExternalStore.

    Even though getSnapshot is called during render, the RFC and documentation imply that React should validate the snapshot after subscribing, since subscribe runs as a passive effect and the store could change between render and effect.

    In this Suspense + StrictMode scenario, subscribe is called without a subsequent getSnapshot check, which can lead to missed updates or tearing for external stores.

    The expectation that getSnapshot runs after subscribe feels reasonable and consistent with the hook’s design goals.

    If this is intentional, it would be helpful to clarify this exception in the documentation. Otherwise, this appears to be a correctness issue specific to Suspense replay semantics.

  2. github-actions commented on Apr 20, 2026

    @github-actions

    This issue has been automatically marked as stale. If this issue is still affecting you, please leave any comment (for example, "bump"), and we'll keep it open. We are sorry that we haven't been able to prioritize it yet. If you have any new additional information, please include it with your comment!

  3. github-actions commented on Apr 27, 2026

    @github-actions

    Closing this issue after a prolonged period of inactivity. If this issue is still present in the latest release, please create a new issue with up-to-date information. Thank you!

  4. kubk commented on Sep 16, 2026

    @kubk

    Seems fixed by #36947

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Resolution: StaleAutomatically closed due to inactivityStatus: UnconfirmedA potential issue that we haven't yet confirmed as a bug

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions