React|Built-in Hook Mastery

useSyncExternalStore & useId: Essential Hooks for Library Developers and SSR

1
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?"

typescript
// โŒ 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().

typescript
// โœ… 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."

  • Examples of External Data: window.innerWidth, navigator.onLine, Redux/Zustand Store, Firestore, etc.
  • 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?

  • The top of the component renders with width: 1000px.
  • (Brief pause -> User resizes window)
  • The bottom of the component renders with width: 500px.
  • 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.

    typescript
    const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?);
  • subscribe: A function to register a callback that gets called when data changes.
  • getSnapshot: A function that returns the current data.
  • getServerSnapshot: (Optional) A function that returns the initial data for SSR environments.
  • Practical Example: Creating a useOnlineStatus Hook

    Let's create a hook that subscribes to the browser's network status (navigator.onLine).

    typescript
    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

  • useId: Generates identical unique IDs on both server and client to prevent Hydration Mismatches. Essential for implementing accessibility (aria-describedby).
  • useSyncExternalStore: Used when subscribing to data outside of React (Browser APIs, Global Stores).
  • Preventing Tearing: Forces synchronous rendering when external data changes to prevent UI inconsistency during concurrent rendering.
  • Usage: While more critical for Library (Store) authors than general application developers, it is extremely useful when wrapping window events in hooks.

  • 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."

    ๐Ÿ”— References

  • React Docs - useId
  • React Docs - useSyncExternalStore
  • What is Tearing? (React 18 Working Group)
  • Comments (0)

    0/1000 characters
    Loading comments...