loke.dev
Header image for The Bridge Between Worlds

The Bridge Between Worlds

How to use the useSyncExternalStore hook to safely subscribe to external data sources without the risk of 'tearing' in concurrent React.

· 4 min read

The Bridge Between Worlds

I once sat in a room with two analog clocks on opposite walls, and they were exactly three minutes out of sync. Every time I looked from left to right, I felt a tiny glitch in my perception of reality, a brief moment where time itself seemed broken. This is exactly what "tearing" feels like in a React app, and it’s why useSyncExternalStore exists.

The Chaos of Concurrent React

Before we talk about the solution, we have to talk about the mess. In the old days of React (pre-v18), rendering was a bit like a steamroller. Once it started, it didn't stop until it was done. You couldn't interrupt it. This was simple, but it meant big updates made your UI feel like it was trapped in molasses.

Then came Concurrent Rendering. Now, React can pause a long-running render, do something more important (like handling a click), and then come back to finish the render.

It’s great for performance, but it’s a nightmare for "external" data. If your data lives inside a React useState or useReducer, React knows how to keep everything consistent. But if your data lives outside—in a Redux store, a Zustand store, or even just the browser's window.innerWidth—you run the risk of tearing.

Imagine React starts rendering a component tree. It reads a value from an external store (Value: A). Then, React pauses to handle an event. During that pause, the external store updates to Value: B. React resumes rendering the rest of the tree and reads Value: B.

Suddenly, half your UI says "A" and the other half says "B." The reality of your app has torn right down the middle.

Enter useSyncExternalStore

This hook is the official bridge. It tells React: "Hey, I know this data lives outside of your bubble, but I want you to treat it with the same respect as internal state."

The signature looks like this:

const state = useSyncExternalStore(
  subscribe, 
  getSnapshot, 
  getServerSnapshot // Optional: only for SSR
);

A Practical Example: The Browser Connectivity Hook

Let’s say you want to track whether the user is online. You *could* do this with a manual useEffect and useState, but you’d be prone to those weird sync issues during heavy renders.

Here is the clean, safe way:

import { useSyncExternalStore } from 'react';

function subscribe(callback) {
  window.addEventListener('online', callback);
  window.addEventListener('offline', callback);
  
  // Return a cleanup function
  return () => {
    window.removeEventListener('online', callback);
    window.removeEventListener('offline', callback);
  };
}

function getSnapshot() {
  return navigator.onLine;
}

function ConnectionStatus() {
  const isOnline = useSyncExternalStore(subscribe, getSnapshot);

  return (
    <div className="p-4 rounded-lg bg-slate-100">
      <p>Status: <strong>{isOnline ? '🌐 Online' : '🚫 Offline'}</strong></p>
    </div>
  );
}

Why this is better than useEffect

You might be thinking, "I've been using useEffect for years and my apps haven't exploded." Fair. But useEffect usually triggers a *second* render after the first one is committed.

useSyncExternalStore is different. If the store changes *while* React is rendering, React can actually catch it and re-start the render to ensure the UI is consistent. It’s synchronous, hence the "Sync" in the name. It forces the UI to stay in lockstep with the data source.

Building a Custom Store

Let's look at a slightly more complex case: a global store you built yourself because you're a rebel who doesn't want another 50kb library.

// Simple store implementation
let state = { count: 0 };
const listeners = new Set();

const store = {
  increment() {
    state = { ...state, count: state.count + 1 };
    listeners.forEach((l) => l());
  },
  subscribe(callback) {
    listeners.add(callback);
    return () => listeners.delete(callback);
  },
  getSnapshot() {
    return state;
  }
};

// Inside your component
function Counter() {
  const { count } = useSyncExternalStore(store.subscribe, store.getSnapshot);

  return (
    <button onClick={store.increment} className="btn-primary">
      Count is {count}
    </button>
  );
}

The "Gotcha" with getSnapshot

There is one rule you absolutely cannot break: `getSnapshot` must return a cached or immutable value if the data hasn't changed.

If you do this, you’ll trigger an infinite loop of renders:

// BAD: Returns a new object every time it's called
getSnapshot() {
  return { value: state.value }; 
}

React calls getSnapshot frequently. If it sees a different object reference, it thinks the store changed, tries to re-render, calls getSnapshot again, sees *another* new object... and your browser tab starts smoking.

Dealing with the Server (SSR)

If you’re using Next.js or any SSR setup, you’ll notice the third argument: getServerSnapshot.

Because there is no window or navigator on the server, your code will crash if you try to access them during the initial render. getServerSnapshot lets you provide a "placeholder" value that matches what the server will generate, preventing hydration mismatch errors.

const isOnline = useSyncExternalStore(
  subscribe,
  getSnapshot,
  () => true // On the server, we just assume they're online
);

When should you use it?

Don't go replacing every useState in your app. This hook is a specialized tool. You should reach for it when:

1. You're writing a library: If you're building a state management library, this is your bread and butter.
2. Browser APIs: Interfacing with window.innerWidth, localStorage, or the History API.
3. Third-party integration: You need to pipe data from a non-React source (like a legacy Backbone model or a WebSocket singleton) into your components.

It’s the boring, reliable plumbing that keeps our modern, concurrent React apps from showing us two different versions of the truth. It’s the bridge that makes sure both of those clocks on the wall finally tick at the exact same time.