React

Verified against Claude Code · 2026-08-02

Replace an ad hoc useEffect subscription with useSyncExternalStore

Migrates a manual useEffect-plus-useState subscription to an external, non-React data source onto useSyncExternalStore, closing the tearing risk under concurrent rendering that the manual pattern cannot rule out.

Claude CodeCursorChatGPTClaude4 fillable variables

The prompt

Ready to copy — highlighted parts are example details you can swap.

You are migrating a component's subscription to an external, non-React data source — a browser API, a third-party SDK, a plain module-level store — from a manual useEffect-plus-useState pattern to useSyncExternalStore, React's dedicated primitive for exactly this case.

EXTERNAL SOURCE
A module-level WebSocket client that maintains a live connectionStatus (connected/reconnecting/disconnected) shared across the whole app.

CURRENT SUBSCRIPTION CODE
useEffect(() => { const handler = s => setStatus(s); wsClient.on("statusChange", handler); setStatus(wsClient.status); return () => wsClient.off("statusChange", handler); }, []);

SNAPSHOT SHAPE
A string enum: 'connected' | 'reconnecting' | 'disconnected' — already a primitive, so no extra caching is needed for reference stability.

SSR REQUIREMENT
Yes — this status indicator appears in a server-rendered header; the WebSocket obviously does not exist during the server render.

MIGRATION RULES
Write a getSnapshot function that reads the current value directly from the external source, synchronously, with no side effects — it must be pure and fast, since React may call it many times per render to check whether the value has changed. The value getSnapshot returns must be either a primitive or a stable reference that only changes identity when the underlying data has actually changed; if getSnapshot constructs a brand-new object or array on every call even when the underlying data is identical, React will conclude the snapshot changed on every single render and useSyncExternalStore will fall into an infinite re-render loop — cache or memoize the snapshot at the source so the same underlying state always produces the same reference. Write a subscribe function that registers a listener with the external source and returns an unsubscribe cleanup function; React calls this once and manages calling your provided callback whenever it needs to re-check the snapshot, so this function's only job is wiring the listener, not tracking any state of its own. Do not keep the old useEffect-plus-useState version running alongside the new hook "just in case" — the entire point of this migration is to remove the specific class of bug the old pattern cannot rule out: under concurrent rendering, a component using useEffect-plus-useState to mirror external state can render with a value that is momentarily inconsistent with what a sibling component reading the same external source renders in the same commit, a phenomenon called tearing, which useSyncExternalStore is specifically designed to prevent by construction. If this component can render on the server, provide a third argument, getServerSnapshot, that returns a safe, deterministic value for the server render — never assume getSnapshot itself is safe to call in an environment where the external source (a browser global, typically) does not exist.

OUTPUT FORMAT
1. The getSnapshot and subscribe functions, as real code, with the stable-reference caching explained inline as a comment where it happens.
2. The updated component using useSyncExternalStore, replacing the removed useEffect and useState entirely.
3. The getServerSnapshot implementation if SSR applies, or an explicit note that it was omitted and why that's safe here.
4. A one-line description of the specific tearing scenario this migration prevents for this exact external source.

Customize

Optional — swap in your own details for the highlighted parts above.

Why this works

The stable-reference requirement on getSnapshot is the single most common bug in a hand-rolled useSyncExternalStore migration, and it is worth stating explicitly rather than assuming: because React calls getSnapshot repeatedly to check for changes, a getSnapshot that builds a new object or array literal on every call — even one that is deeply equal to the last one — reads as "changed" every single time under React's default reference-equality check, which produces an infinite render loop that looks like a performance bug but is actually a correctness bug in the snapshot function itself, and it is exactly the kind of mistake a model will make by default when translating a useState value that used to be freely recreated into a getSnapshot function with no comment warning against it. Tearing — the actual problem useSyncExternalStore exists to solve — is a real, documented consequence of concurrent rendering that the plain useEffect-plus-useState pattern genuinely cannot rule out: React can pause a render mid-way through, let an external mutation happen, and resume, which means two components both mirroring the same external value through separate useEffect subscriptions can end up rendering different values within what the user experiences as a single, atomic UI update, and useSyncExternalStore is specifically the primitive React itself uses internally, and that libraries like Redux and Zustand build on, to guarantee all consumers of one external source stay consistent within a single render. Requiring an explicit getServerSnapshot rather than letting getSnapshot run unguarded on the server closes a specific and common SSR crash: a getSnapshot that reads window.someGlobal or a similar browser-only value throws immediately during a server render where that global was never defined, and a model that has only ever seen this pattern used client-side will not reach for the third argument unless it's asked to consider SSR explicitly. Removing the old useEffect-plus-useState version entirely, rather than leaving it in place "just in case," matters because a codebase with both patterns subscribing to the same external source side by side is worse than either one alone: the two can now individually pass their own tests while still tearing relative to each other, since useSyncExternalStore's consistency guarantee only covers components that actually use it, and a straggling useEffect-based subscriber elsewhere is exactly the kind of leftover that undoes the whole point of migrating in the first place.

What you get back

function subscribe(callback) { wsClient.on('statusChange', callback); return () => wsClient.off('statusChange', callback); } function getSnapshot() { return wsClient.status; // already a primitive string — no extra caching needed for reference stability } function getServerSnapshot() { return 'disconnected'; // deterministic, safe default — the WebSocket never exists during SSR }

function ConnectionIndicator() { const status = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); return <span className={statusStyles[status]}>{status}</span>; }

Tearing scenario this prevents: without useSyncExternalStore, a header indicator and a footer indicator both mirroring wsClient.status through separate useEffect calls could, under concurrent rendering, briefly show "connected" and "reconnecting" simultaneously within the same commit if the status changed mid-render — useSyncExternalStore guarantees both read the identical value in that render.

Verified against

Claude Code Sonnet 4.6 · 2026-08-02

Claude Sonnet 4.6 · 2026-08-07

Changelog

  • 2026-08-02 — Initial publish, verified against Claude Code and Claude on Sonnet 4.6.

Need this built into your business?

If a prompt isn't enough — custom software, built and maintained for you — that's Scult's day job.

EXPLORE CUSTOM SOFTWARE
All React prompts

Check your AI visibility

One URL in, a 0–100 score and the exact fixes out.

RUN THE CHECK

Browse all the tools

15 tools across six categories
13 of them never send your data anywhere

Free · No signup · No trial clock

SEE THE DIRECTORY