Skip to content

Answer set-state-in-effect instead of downgrading it - #160

Merged
abernier merged 1 commit into
mainfrom
fix/set-state-in-effect
Aug 9, 2026
Merged

Answer set-state-in-effect instead of downgrading it#160
abernier merged 1 commit into
mainfrom
fix/set-state-in-effect

Conversation

@abernier

@abernier abernier commented Aug 9, 2026

Copy link
Copy Markdown
Member

eslint-config-next@16 brought eslint-plugin-react-hooks 7, and the Next 16 bump silenced its new set-state-in-effect rule app-wide with a "warn" override, with a comment promising a later refactor. This is that refactor: the override is gone and every report is answered.

What changed in Nav.tsx

was is
mount effect reading data-nav-collapsed off <html> useSyncExternalStore, with a server snapshot so the first render matches the prerendered HTML — the attribute already exists at hydration, it was never something to sync to
one-shot effect + ref revealing the rail on a shared filter link the render-phase adjustment React documents for "a value changed and some state has to follow" — lands in the same commit, no second pass
useEffect(() => setReady(true), []) (×2) useIsClient()
mousemove effect that also reset near when the rail opened useEventListener, with the reset folded into the handler
one keydown listener with a ladder of early returns, re-subscribed on every keystroke five useHotkeys, where enabled / ignoreEventWhen say out loud what the ladder said by falling through

useNearbyExamples keeps its measure-then-render pass behind a single scoped eslint-disable: that is the one thing useLayoutEffect exists for, and the one shape the rule cannot tell apart from a cascading render. Folding the no-IntersectionObserver fallback into the same unbounded seed means only one setNearby needs the exemption.

hooks/use-mobile.ts is shadcn's and stays stock, so the rule is switched off for that file alone — it cannot simply be deleted from the config, since the rule is an error by default in the plugin's recommended set.

Also

  • The two react-hooks/exhaustive-deps warnings the override left standing (setSearch missing from dismissSearch and from the keydown effect — the latter now moot).
  • Tailwind classes that have a canonical form: duration-[1078ms]duration-1078, md:[margin-inline-start:var(--nav-offset)]md:ms-(--nav-offset), [&_[data-slot=…]]:**:data-[slot=…]:, [&>[data-slot=…]]:*:data-[slot=…]:. Byte-identical declarations and equal specificity, checked against the built stylesheet before and after.

New deps: usehooks-ts, react-hotkeys-hook.

Verified

pnpm lint clean (no warnings), tsc --noEmit clean, next build (static export) green. In the browser, on both the export and the dev server, with no hydration warnings: collapse preference survives a reload with no flash; a ?q= link opens a rail that was stored collapsed without overwriting that preference; Escape clears the filter and the rail stays open; type-to-search, Backspace, / and ⌘K all behave as before.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Y9J9K5TobdJo49EU4Z1Fp4

`eslint-config-next@16` brought eslint-plugin-react-hooks 7, whose
`set-state-in-effect` rule the Next 16 bump silenced repo-wide with a
`"warn"` override. Drop the override and take the reports:

- The rail's collapse state came out of a mount effect reading the
  attribute the pre-paint script leaves on <html>. It is a value that
  already exists at hydration, so `useSyncExternalStore` reads it, with a
  server snapshot keeping the first render matching the prerendered HTML.
- The shared-filter-link reveal was a one-shot effect over a ref; it is
  now the render-phase adjustment React documents for exactly this.
- The pill's "mounted yet?" and pointer-proximity effects, plus the
  `keydown` listener that was torn down and re-subscribed on every
  keystroke, are now `useIsClient`, `useEventListener` and `useHotkeys`.
  What the listener's ladder of early returns said by falling through,
  `enabled` and `ignoreEventWhen` now say out loud.
- `useNearbyExamples` keeps its measure-then-render pass — the one thing
  `useLayoutEffect` is for, and the one shape the rule cannot tell apart
  from a cascading render — behind a single scoped disable. Folding the
  no-IntersectionObserver fallback into the same unbounded seed leaves
  only that one `setNearby` to exempt.
- `hooks/use-mobile.ts` is shadcn's, kept stock, so the rule is switched
  off for that file alone rather than for the app.

Also fixes the two `exhaustive-deps` warnings the override left visible,
and the Tailwind classes with a canonical form (`duration-1078`,
`ms-(--nav-offset)`, `**:data-[slot=…]`) — same CSS, verified against the
built stylesheet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y9J9K5TobdJo49EU4Z1Fp4
@abernier
abernier merged commit ac85064 into main Aug 9, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant