useSyncExternalStore & useId: Essential Hooks for Library Developers and SSR

Chris is currently building an accessible input component for a Next.js project.
To connect the input and label for screen readers, a unique ID is required.
"If I generate an ID with random numbers, there won't be any collisions, right?"
// โ Chris's Dangerous Idea
function AccessibleInput() {
const id = `input-${Math.random()}`; // Generated randomly every render
return (
<>
<label htmlFor={id}>Name</label>
<input id={id} type="text" />
</>
);
}As soon as he opens the browser console, Next.js throws a red warning.
"Hydration failed because the initial UI does not match what was rendered on the server."
This happens because the ID generated during server-side rendering (input-0.123) differs from the ID generated during client-side hydration (input-0.987).
Let's explore two advanced hooks that solve these SSR mismatch issues and the problem of safely fetching data from outside React (stores, browser APIs).
1. useId: A Unique ID Generator Safe for SSR
useId is a hook introduced in React 18 that generates unique IDs that are consistent between the server and the client.
Internally, it generates IDs based on the position in the component tree (e.g., :r1:, :r2:). Therefore, as long as the call order remains the same, it guarantees the exact same string on both the server and the client.
Solution: Fixing the Accessible Component
Chris replaced Math.random() with useId().
// โ
Applying useId
import { useId } from 'react';
function AccessibleInput() {
const id = useId(); // Generates e.g., ":r1:"
return (
<>
<label htmlFor={id}>Name</label>
<input id={id} type="text" aria-describedby={`${id}-hint`} />
<p id={`${id}-hint`}>Please enter your real name.</p>
</>
);
}Now, the hydration error is gone.
useId is not only useful for connecting form elements but also for defining IDs for SVG gradients (defs) or animation elements.
Warning: Do not use useId for list key props. (If data changes, the order might get messed up).
2. useSyncExternalStore: Safely Syncing with the Outside World
This hook has a long and somewhat intimidating name, but its role is simple.
"It prevents concurrency issues (Tearing) that can occur when subscribing to external data, rather than React's internal State."
The Problem: Tearing
React 18's Concurrent Rendering splits the rendering process into chunks.
What happens if external data (e.g., window.width) changes during a render?
This phenomenon, where different data is displayed on a single screen, is called Tearing.
Solution: How to use useSyncExternalStore
This hook forces React to say, "Stop rendering right now, sync immediately, and redraw!" whenever external data changes. It prioritizes data Consistency, even if it means sacrificing some concurrency features.
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?);Practical Example: Creating a useOnlineStatus Hook
Let's create a hook that subscribes to the browser's network status (navigator.onLine).
import { useSyncExternalStore } from 'react';
// 1. Function to get the current state (Snapshot)
function getSnapshot() {
return navigator.onLine;
}
// 2. Function to subscribe to state changes (Subscribe)
function subscribe(callback: () => void) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
// Cleanup function is mandatory
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}
export function useOnlineStatus() {
// โ
Safely sync external data (navigator.onLine) with React
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
return isOnline;
}Now, there is no need to manage complex event listeners with useState and useEffect inside React. State management libraries like Redux and Zustand all use this hook internally to ensure compatibility with React 18/19.
Key Takeaways
This concludes Built-in Hook Mastery.
We have acquired every weapon React offers, starting from useState to React 19's latest useActionState and useOptimistic.
Now it's time to combine these weapons to create our own arsenal.
Repetitive input handlers, tedious boolean toggle logic, confusing localStorage integration...
Let's depart for the world of Custom Hooks, which reduce all of this to a single line.
Continuing in the first topic of Essential Custom Hooks: "useBoolean & useToggle: The Smallest but Most Used Hooks."