TLDR: Don't silence
react-hooks/set-state-in-effectwitheslint-disable. It's the lint telling you your SSR will break. The real fix isuseSyncExternalStore— a single hook that kills both problems at once.
The setup
I was rebuilding my personal site on Next.js 16 — new scaffold, fresh create-next-app, all the modern niceties.
One of the first things I wired up was a mode toggle that reads from localStorage: a 'days'/'weeks' preference for how content gets grouped.
Classic pattern, right?
const [mode, setMode] = useState<DurationMode>('days')
useEffect(() => {
const saved = window.localStorage.getItem(storageKey)
if (saved === 'days' || saved === 'weeks') setMode(saved)
}, [storageKey])
Written it a hundred times. Harmless.
What broke
Two things, independently, both bad.
First: hydration mismatch (Next.js, my server-rendered app). The server renders 'days'. The client's first paint reads the saved value from localStorage — maybe 'weeks'. React reconciles and throws. Blink. Flash. Screaming red console.
Second: eslint-config-next@16 (which ships with React Hooks plugin v6) now treats react-hooks/set-state-in-effect as a hard ERROR, not a warning. Calling setState(...) synchronously inside a useEffect body — no matter how innocent it looks — is blocked cold.
Your build doesn't catch it. eslint . has 10+ errors you never knew existed.
What I tried first (the wrong thing)
I did what most people do.
I added // eslint-disable-next-line react-hooks/set-state-in-effect above the useEffect and committed it.
The actual commit message: fix(lint): suppress SSR-required setState-in-effect.
I literally named it "suppress."
CI went green. The lint was quiet. I moved on.
The problem is: suppressing the lint doesn't fix the hydration mismatch. Server still renders 'days'. Client still snaps to 'weeks' on first paint. I just stopped the tooling from yelling about it.
(Worth noting: the disable comment has to go directly above the setState(...) call, not above the useEffect — that's a sneaky gotcha. Putting it on the wrong line doesn't disable anything inside the body.)
The fix that actually works
A few weeks later I hit the same pattern on a client project — a useDurationMode hook, an ecommerce client's supply-chain app — and this time I dug in properly.
useSyncExternalStore (React 18+, built right into the standard library — no package needed) is the canonical React tool for this. You give it three things:
- A
subscribefunction — so React knows when to re-render - A
getSnapshot— reads the current value on the client - A
getServerSnapshot— what the server should return
That third argument is the whole game.
const mode = useSyncExternalStore(
subscribe,
() => readMode(storageKey), // client snapshot
() => 'days' // server snapshot — always the default
)
Server renders 'days'. Client's first render also returns 'days' (the hook says so). React reconciles: they match. No mismatch. No flash.
Then, on the second client render, the hook reads the real localStorage value and updates. Smooth. Correct. No effect. No lint error.
Why it works: the mismatch happens because server HTML ≠ client first paint. getServerSnapshot makes them identical by definition. There's nothing to reconcile.
Why it matters to me
I shipped a suppressed lint rule on my own site because I didn't want to slow down.
That's the honest version.
The rule isn't just a style preference — it's React's way of saying you're setting state in a way that can cause cascading renders and SSR mismatches. Silencing it doesn't make those problems go away; it just makes them quieter until they aren't.
The pattern I reach for now: any hook that reads browser state (localStorage, sessionStorage, window anything) gets useSyncExternalStore from the start. It's maybe 8 more lines than useState + useEffect. It costs nothing. It buys you correctness, lint compliance, and zero hydration drama.
Don't suppress. Fix.
P.S. If you're fetching data when a prop changes — not reading browser state — reach for SWR instead. Same instinct, different tool. The
useState + useEffectreflex almost never wins in React 19.