A Better useSSR Implementation

Published See discussion on Bluesky

For teams working within a server side rendered react app (like when using Next.js or Remix), I often see a useSSR (or useClient) hook within the codebase.

These hooks usually return a boolean indicating if we're on the client or being server rendered - this can be useful to introduce some functionality when we are on the client and now have access to the window or navigator globals for example, or can be a useful way to render different UI on the server or the client!

Most of these implementations look something like this:

1import {useState, useEffect} from 'react';
2
3export function useSSR() {
4 let [mounted, setMounted] = useState(false);
5
6 useEffect(() => {
7 setMounted(true)
8 }, [])
9
10 return mounted;
11}
1import {useState, useEffect} from 'react';
2
3export function useSSR() {
4 let [mounted, setMounted] = useState(false);
5
6 useEffect(() => {
7 setMounted(true)
8 }, [])
9
10 return mounted;
11}

This implementation isn't all that bad - it's relatively easy to understand what's happening!

However, this implementation causes a double render on the client - which can sometimes be less than ideal. The first render is the hydration pass on the server rendered HTML, the second render is after the setState is applied within the useEffect.

There's now a better way to implement this within React: useSyncExternalStore!

The name is pretty confusing - it was original built to help external stores (e.g. Redux, Jotai, etc) integrate better with React's scheduler. However it is pretty flexible to be used for a few other use cases as well!

Here's the new implementation using useSyncExternalStore:

1import {useSyncExternalStore} from 'react';
2
3let emptySubscribe = () => () => {};
4
5export function useSSR() {
6 return useSyncExternalStore(
7 emptySubscribe,
8 // client value:
9 () => true,
10 // server value:
11 () => false,
12 );
13}
1import {useSyncExternalStore} from 'react';
2
3let emptySubscribe = () => () => {};
4
5export function useSSR() {
6 return useSyncExternalStore(
7 emptySubscribe,
8 // client value:
9 () => true,
10 // server value:
11 () => false,
12 );
13}

This will return the same values as our old implementation - but skips the need for a re-render after an effect runs. React does the heavy lifting for that now with this new hook!

I first saw this referenced from this tweet: