🔍 What Is Hydration?
Hydration is the process in which React matches the HTML output generated on the server with interactive components in the browser.
In other words, when the page first loads, the user actually sees static HTML. Then React “brings it to life” and makes things like buttons and inputs interactive.
Sometimes, however, the HTML rendered on the server and the React tree built on the client don't match exactly. When that happens, React throws a “hydration mismatch” error or is forced to re-render the component.
💡 What Is useSyncExternalStore?
useSyncExternalStore is a hook introduced in React 18.
It lets you work with external data sources (e.g. a global store, an event emitter, etc.) in a way that's compatible with SSR + hydration.
Its basic usage looks like this:
const isSSR = React.useSyncExternalStore(
() => () => {}, // subscribe: değişiklikleri dinler
() => false, // getSnapshot: istemci tarafında çağrılır
() => true // getServerSnapshot: sunucu + hydration sırasında çağrılır
)
With this setup, React keeps the output consistent between the server and the client.
That is, during hydration it returns exactly the value that's expected, so no mismatch occurs.
⚠️ Pitfalls Between useSyncExternalStore and Suspense
While useSyncExternalStore may look like a great solution, some problems show up, especially when it's used together with the Suspense component:
- Flickering Suspense fallbacks
The page first shows the server output, then a loading spinner appears, and then the real content comes back. From the user's point of view, this “flicker” isn't a pleasant experience. - Updates reaching components that haven't hydrated yet
If the store changes before some components become interactive, hydration errors can occur. In that case, React is forced to re-render. - Poor INP (Interaction to Next Paint) scores
BecauseuseSyncExternalStoretriggers synchronous rendering, it blocks the main thread. This causes delays in interactions. - Loss of control over render timing
The hook disables React's “time slicing” advantage, which can hurt performance.
🧩 The Solution: Using It Together with useDeferredValue
This is where the useDeferredValue hook comes in to solve these problems.
By handling the value returned from useSyncExternalStore in a deferred way, we can prevent both hydration mismatches and performance issues.
Here's an example implementation:
const emptySubscribe = () => () => {}
const returnFalse = () => false
const trueOnServerOrHydration = () => true
function useIsSSR() {
const isSSRSync = React.useSyncExternalStore(
emptySubscribe,
returnFalse,
trueOnServerOrHydration
)
return React.useDeferredValue(isSSRSync)
}
function App() {
const isSSR = useIsSSR()
return isSSR ? "Server output" : "Client output"
}
🔍 How Does It Work?
- On the first render (the
SSR + hydrationphase),isSSRistrue.
This way, React uses the output from the server directly → no mismatch. - React then produces
isSSRSync = falseon the client side. - But
useDeferredValuedoesn't reflect this change right away → the user doesn't see a “flickering fallback”. - After that, React switches to the client-side content in a non-blocking (concurrent) way.
As a result:
- Hydration errors are prevented ✅
- Suspense runs more smoothly ✅
- INP is preserved and the user experience gets smoother ✅
🧠 Bonus: Improving Stability with Memoization
On top of that, the article recommends keeping component references stable with tools like useMemoOne.
This improves performance by preventing unnecessary re-renders across Suspense boundaries.
🚀 Conclusion
In React, SSR and hydration are critical for giving users a fast and seamless initial load experience.useSyncExternalStore is a powerful tool for keeping this process under control — but if it isn't used correctly, it can lead to performance problems.
The best approach is to use this hook together with useDeferredValue.
That way, you can get rid of hydration errors and deliver a smoother user experience.
Leave a comment
Your email address will not be published. Required fields are marked *