useSyncExternalStore — React Hook

AkashSDas
3 min readMar 29, 2024

--

The useSyncExternalStore hook allows us subscribe to an external store. Its useful when we want to connect to systems outside of React.

It was introduced in React v18, and tts mostly meant for library authors rather than app authors.

Following is the signature of useSyncExternalStore:

const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)

It returns the snapshot of the data in the store. We’ve to pass in subscribe function which will subscribe to the store (also return a function that will be used to unsubscribe), and a getSnapshot function which will get a snapshot of the data from the store.

A look on the parameters:

  • subscribe: A function that takes a single callback argument and subscribes it to the store. When the store changes, it should invoke the provided callback. This will cause the component to re-render. The subscribe function should return a function that cleans up the subscription.
  • getSnapshot: A function that returns a snapshot of the data in the store that’s needed by the component. While the store has not changed, repeated calls to getSnapshot must return the same value. If the store changes and the returned value is different (as compared by Object.is), React re-renders the component.
  • optional getServerSnapshot: A function that returns the initial snapshot of the data in the store. It’ll be used only during server rendering and during hydration of server-rendered content on the client. The server snapshot must be the same between the client and the server, and is usually serialized and passed from the server to the client. If we omit this argument, rendering the component on the server will throw an error.

The useSyncExternalStore returns the current snapshot of the store which we can use in our rendering logic.

There’re some caveats with useSyncExternalStore.

Below is an example of useSyncExternalStore hook’s usage where we’ve a todo feature and user online status. Both of them are outside the React app.

import { useState, useSyncExternalStore } from "react";

// ======================================
// STORE
// ======================================

let nextId = 0;
let todos = [
{ id: nextId++, text: "Learn React" },
{ id: nextId++, text: "Learn TypeScript" },
];
let listeners: CallableFunction[] = [];

const todoStore = {
addTodo: function (text: string) {
// todos.push({ id: nextId++, text }); // This is wrong and won't work
todos = [...todos, { id: nextId++, text }];
console.log("[addTodo] todos", todos);
listeners.forEach((listener) => listener());
},
getSnapshot: function () {
console.log("[getSnapshot] todos", todos);
return todos;
},
subscribe: function (listener: CallableFunction) {
listeners.push(listener);
console.log("[subscribe] listeners", listeners);

return function unsubscribe() {
console.log("[unsubscribe] listeners", listeners);
listeners = listeners.filter((l) => l !== listener);
};
},
};

// ======================================
// COMPONENT
// ======================================

export default function App(): JSX.Element {
const [show, setShow] = useState(false);

return (
<div>
<OnlineStatus />
<button onClick={() => setShow(!show)}>Toggle</button>
{show && <Todos />}
</div>
);
}

function Todos(): JSX.Element {
const todos = useSyncExternalStore(
todoStore.subscribe,
todoStore.getSnapshot
);

return (
<div>
<h2>Good morning</h2>

<ul>
{todos.map((todo) => (
<li key={todo.id.toString()}>{todo.text}</li>
))}

<li>
<button onClick={() => todoStore.addTodo("Learn Hooks")}>
Add Todo
</button>

<button onClick={() => todoStore.addTodo("Learn Redux")}>
Add Todo
</button>

<button onClick={() => todoStore.addTodo("Learn MobX")}>
Add Todo
</button>
</li>
</ul>
</div>
);
}

function OnlineStatus(): JSX.Element {
const isOnline = useOnlineStatus();

return (
<div>
<h2>Good morning</h2>

<p>Are you online? {isOnline ? "Yes" : "No"}</p>
</div>
);
}

function useOnlineStatus() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
return isOnline;
}

function getSnapshot() {
return navigator.onLine;
}

function subscribe(callback: () => void) {
window.addEventListener("online", callback);
window.addEventListener("offline", callback);

return function unsubscribe(): void {
window.removeEventListener("online", callback);
window.removeEventListener("offline", callback);
};
}

--

--