Render as you fetch (with and without suspense)
Speed up your app's loading of code/data/assets with "render as you fetch" with and without React Suspense for Data Fetching

React's useSyncExternalStore
is one of those hooks that most developers don't
reach for every day, but when you need it, you really need it. It's crucial
for integrating React components with external state management systems or
browser APIs that React doesn't control. Unfortunately, it's also frequently
misunderstood. Let's clear up the confusion, walk through real-world examples,
and tackle the most common mistakes.
React's built-in state (useState
, useReducer
) and Context API are excellent
for managing data that lives inside your React application. But what happens
when your component needs to display data from a source outside React's
control? Consider:
navigator.onLine
(online status),
document.visibilityState
(page visibility), window.matchMedia
(media
queries).useSyncExternalStore
internally for their React bindings).Before useSyncExternalStore
, developers typically used useEffect
and
useState
to subscribe to these external sources and update local component
state. While this worked in simpler cases, it could lead to inconsistencies with
React's concurrent rendering capabilities.
Concurrent rendering allows React to work on multiple UI updates simultaneously. Pausing, resuming, or abandoning rendering work as needed. This significantly improves perceived performance. However, if an external store changes while React is in the middle of rendering a component tree, different components might read different versions of the external data. This inconsistency is called "tearing," where your UI literally "tears" by showing conflicting information.
useSyncExternalStore
solves this by providing a React-managed, synchronous way
to subscribe to an external store. It guarantees that all components in a render
pass see the same, consistent snapshot of the data, even during concurrent
updates, thus preventing tearing.
const synchronizedState = useSyncExternalStore( subscribe, getSnapshot, getServerSnapshot? // Optional);
Let's take a closer look at the arguments:
subscribe(callback)
:
callback
function provided by React.subscribe
function
must call this callback
. This informs React that the store has
changed and a re-render might be necessary.unsubscribe
function. React will call this cleanup
function when the component unmounts or if the subscribe
function itself
changes between renders.getSnapshot()
:
getSnapshot
should
return the same value by reference (if it's an object or array) or the
same primitive value as the last time it was called. This allows React to
optimize re-renders using Object.is
comparison. More on this in the
"Common Mistakes" section.getServerSnapshot?()
(Optional):
Let's create a custom hook useOnlineStatus
to track if the user's browser is
online.
import { useSyncExternalStore } from 'react'// 1. Define getSnapshot outside the component:// It reads the current state from the external source.function getOnlineStatusSnapshot() { return navigator.onLine}// 2. Define subscribe outside the component:// It sets up listeners and calls the React-provided callback on change.function subscribeToOnlineStatus(callback) { window.addEventListener('online', callback) window.addEventListener('offline', callback) // Return the cleanup function return () => { window.removeEventListener('online', callback) window.removeEventListener('offline', callback) }}// 3. Create the custom hookexport function useOnlineStatus() { // useSyncExternalStore ensures synchronous reads and tearing prevention const isOnline = useSyncExternalStore( subscribeToOnlineStatus, getOnlineStatusSnapshot, // For a robust SSR scenario, you should provide getServerSnapshot: () => true, // Assuming client is 'online' or a sensible default ) return isOnline}// Usage in a component:function StatusBar() { const isOnline = useOnlineStatus() return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>}
This example demonstrates the core pattern: stable subscribe
and getSnapshot
functions interacting with the external source (navigator.onLine
and its
events).
People have asked me lots of questions about useSyncExternalStore
. Here are
some of the most common ones.
useEffect
+ useState
?"While useEffect
and useState
can subscribe to external stores, this pattern
is prone to tearing in concurrent rendering. React might pause rendering a
component, an external store might update, and then React might resume rendering
with stale data or cause inconsistencies across the UI. useSyncExternalStore
is specifically designed to integrate with React's rendering lifecycle, ensuring
that reads are synchronous and consistent during a render pass. If you're
syncing with state outside React, useSyncExternalStore
is the correct, robust
solution.
subscribe
or getSnapshot
functions are re-created on every render!"If you define subscribe
or getSnapshot
inline within your component or
custom hook without memoization, they will be new functions on every render.
// ❌ Bad: subscribe and getSnapshot are re-created every renderfunction MyComponentUsesStore() { // These functions get new identities on each render of MyComponentUsesStore function subscribe(callback) { /* ... */ } function getSnapshot() { /* ... */ } const value = useSyncExternalStore(subscribe, getSnapshot) // ...}
When useSyncExternalStore
receives a new subscribe
function instance, it
will re-subscribe to the store (calling the old unsubscribe function first, then
the new subscribe). This is inefficient and can lead to bugs or memory leaks if
not handled perfectly.
Solution:
useOnlineStatus
example. This is the simplest and often best way.useCallback
: If your subscribe
or getSnapshot
functions depend on props or state (e.g., an ID to subscribe to a specific
document), wrap them in useCallback
.
// ✅ Good: subscribe and getSnapshot are stablefunction subscribeToStore(callback) { /* ... */}function getStoreSnapshot() { /* ... */}function MyComponentUsesStore() { const value = useSyncExternalStore(subscribeToStore, getStoreSnapshot) // ...}// ✅ Also Good (if dependent on props, e.g., storeId):import { useCallback, useSyncExternalStore } from 'react'function MyComponentUsesStore({ storeId }) { const subscribe = useCallback( (callback) => { return externalStoreAPI.subscribe(storeId, callback) }, [storeId], ) const getSnapshot = useCallback(() => { return externalStoreAPI.getSnapshot(storeId) }, [storeId]) const value = useSyncExternalStore(subscribe, getSnapshot) // ...}
getSnapshot
called so many times?"React may call getSnapshot
multiple times during a render pass, and
potentially even if no re-render occurs (e.g., to verify consistency). This is
expected behavior. Therefore, getSnapshot
must be:
getSnapshot
must return the exact same value (referential equality
for objects/arrays using Object.is
).React uses the return value of getSnapshot
to determine if a re-render is
needed. If it frequently returns new object/array instances even for unchanged
data, it will cause unnecessary re-renders (see mistake #8 below).
useSyncExternalStore
is not a function."This almost always means you're using a version of React older than 18.
useSyncExternalStore
was introduced in React 18. Solution: Upgrade your
react
and react-dom
packages to at least v18.0.0
.
If your external store can provide a meaningful value on the server, you
must provide the third argument, getServerSnapshot
. If you don't, you'll
get the following error in the console:
Missing getServerSnapshot, which is required for server-rendered content. Willrevert to client rendering.
Here's an example of how to use getServerSnapshot
:
const value = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
getServerSnapshot
should return the initial state of the store as it should be
rendered on the server. For example, if you're syncing with localStorage
(which doesn't exist on the server), getServerSnapshot
might return null
or
a default value. Just know that this could cause a flash of incorrect content.
So you'll want to come up with a creative design to handle this (or even better,
use a cookie or a server-side state management solution for this).
If you omit getServerSnapshot
and the component renders on the server, React
expects the initial client-side render (after hydration) to match the
server-rendered HTML. If getSnapshot()
on the client returns something
different from what was implicitly rendered on the server (or if the server
can't render it), you'll get a hydration mismatch error. If the component using
the hook suspends on the server (e.g. because it can't get a value), then it
will also suspend on the client during hydration until the subscribe
function
is called and a client-side snapshot is available.
Yes, absolutely! This hook is essential for safely using external stores in such environments.
getServerSnapshot
.window.matchMedia
):
getServerSnapshot
should return a sensible default (e.g., false
for a
media query, or true
for navigator.onLine
).getSnapshot
will then provide the actual browser value
upon hydration. React will ensure a smooth transition.window.matchMedia
:
// In your hookconst getServerSnapshot = () => false // Or whatever default makes sense// ...useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
<Suspense>
if not
providing getServerSnapshot
and expect it to suspend during hydration.This is a common point of confusion.
useSyncExternalStore
internally to bridge their external store logic with
React's concurrent rendering. As an end-user of these libraries, you
typically use their provided hooks (useSelector
, useStore
) and don't call
useSyncExternalStore
directly.You, as an application developer, would directly use useSyncExternalStore
when:
navigator.onLine
,
document.visibilityState
, window.matchMedia
, etc.useSyncExternalStore
is the primitive you'd use
to make it compatible with React's concurrent features.So, it's not an "either/or" with libraries like Zustand; rather,
useSyncExternalStore
is often an implementation detail for them or a tool
for you when those libraries aren't the right fit for interfacing with a
particular external data source.
getSnapshot
?"React uses Object.is()
to compare the previous snapshot with the current one
returned by getSnapshot
. If getSnapshot
returns a new object or array
reference on every call, React will think the state has changed, leading to
re-renders even if the underlying data is identical.
// External store (example)const myExternalStore = { _data: { user: { name: 'Alex', preferences: { theme: 'dark' } } }, listeners: [], getData() { return this._data }, subscribe(listener) { /* ... */ return () => { /* ... */ } }, // ... methods to update _data and notify listeners}// ❌ Bad: getSnapshot always returns a new objectfunction getPreferencesSnapshot_Bad() { // Even if preferences haven't changed, this is a new object instance every time. return { ...myExternalStore.getData().user.preferences }}// ✅ Good: Return the same object reference if data hasn't changed.// This requires your store or your snapshot logic to be a bit smarter.// Option 1: If the store itself manages immutable data for selections.function getPreferencesSnapshot_Good_Immutable() { // Assumes myExternalStore.getData().user.preferences *is* the immutable object, // replaced only when it actually changes. return myExternalStore.getData().user.preferences}// Option 2: Manually cache the derived snapshot.let lastKnownPreferences = myExternalStore.getData().user.preferenceslet cachedPreferencesSnapshot = { ...lastKnownPreferences }function getPreferencesSnapshot_Good_Cached() { const currentPreferences = myExternalStore.getData().user.preferences // Shallow comparison; for deep objects, you might need a deep comparison // or ensure the store replaces `currentPreferences` by reference on any nested change. if (currentPreferences !== lastKnownPreferences) { cachedPreferencesSnapshot = { ...currentPreferences } lastKnownPreferences = currentPreferences } return cachedPreferencesSnapshot}
The key is referential stability: If the data didn't change, getSnapshot
must return the exact same object instance as before. If it's a primitive
(string, number, boolean), this is less of an issue unless you're re-computing
it unnecessarily.
useMediaQuery
HookHere's how you can build a reusable hook for media queries, ensuring subscribe
and getSnapshot
are stable using useCallback
if the query can change. This
is from the Epic React workshop on
Advanced React APIs.
import { Suspense, useSyncExternalStore } from 'react'import * as ReactDOM from 'react-dom/client'export function makeMediaQueryStore(mediaQuery: string) { function getSnapshot() { return window.matchMedia(mediaQuery).matches } function subscribe(callback: () => void) { const mediaQueryList = window.matchMedia(mediaQuery) mediaQueryList.addEventListener('change', callback) return () => { mediaQueryList.removeEventListener('change', callback) } } return function useMediaQuery() { return useSyncExternalStore(subscribe, getSnapshot) }}const useNarrowMediaQuery = makeMediaQueryStore('(max-width: 600px)')function NarrowScreenNotifier() { const isNarrow = useNarrowMediaQuery() return isNarrow ? 'You are on a narrow screen' : 'You are on a wide screen'}function App() { return ( <div> <div>This is your narrow screen state:</div> <Suspense fallback="...loading..."> <NarrowScreenNotifier /> </Suspense> </div> )}const root = ReactDOM.hydrateRoot(rootEl, <App />, { onRecoverableError(error) { if (String(error).includes('Missing getServerSnapshot')) return console.error(error) },})
Note: In the useMediaQuery
example, useEffect
with useState
might seem
simpler for client-only scenarios if you are not in concurrent mode or don't
deeply care about tearing for this specific feature. However,
useSyncExternalStore
is the most robust way to handle this, especially with
SSR and concurrent features. Notice that even though we're assuming server
rendering (with hydrateRoot
), we're not supplying a getServerSnapshot
because we don't have a way to check the media query on the server. So we add a
onRecoverableError
handler to avoid the unnecessary error logging.
When debugging useSyncExternalStore
:
subscribe
and getSnapshot
functions stable
(defined outside the component or memoized with useCallback
)?getSnapshot
Purity & Performance: Is getSnapshot
fast, pure, and does
it return the same value reference (Object.is
true) if the underlying data
hasn't changed?subscribe
Correctness: Does subscribe
correctly call the
React-provided callback
only when the store actually changes? Does it
return a proper unsubscribe
function?getServerSnapshot
function? Does
it return a value consistent with what the client might see initially or a
safe default?useSyncExternalStore
is a specialized but powerful hook for safely connecting
React components to external data sources in the concurrent era. By
understanding its purpose (to prevent tearing and ensure consistent reads) and
by following these best practices, you can confidently integrate React with any
external state management system or browser API.
useSyncExternalStore
Happy (and safe) syncing! And check out the Epic React workshop on Advanced React APIs for more advanced patterns and use cases!
Delivered straight to your inbox.
Speed up your app's loading of code/data/assets with "render as you fetch" with and without React Suspense for Data Fetching
Master focus management in React with flushSync. Learn why batching matters, when to use flushSync, and how to create accessible, keyboard-friendly UIs.
Control Props let you hand over component state to your users—just like a controlled input. Learn to build ultra-flexible UIs at EpicReact.dev.
Make your React apps feel instant with the new useOptimistic hook from React 19. Improve UX by updating UI immediately while async actions complete.