Skip to content

Eliminate setState-in-effect anti-patterns across 16 sites #200

Description

@JohnRDOrazio

Context

eslint-config-next 16 introduced the react-hooks/set-state-in-effect rule, which flags setState calls made synchronously inside a useEffect body. This pattern produces cascading renders, makes data flow harder to reason about, and is almost always a sign that the state can be derived, lifted, or assigned during the triggering event instead.

The rule is currently set to "warn" in eslint.config.mjs:9-11 so it doesn't gate CI:

// TODO: address these new react-hooks rules from eslint-config-next 16
// See: https://react.dev/learn/you-might-not-need-an-effect
"react-hooks/set-state-in-effect": "warn",
"react-hooks/immutability": "warn",

This issue tracks the cleanup of all current set-state-in-effect warnings so the rule can graduate to "error".

Scope

16 unique sites across 12 files. (Category A ended up with 6 sites, not 7 — one entry moved to Category D; see below.) Grouping by remedy makes the work tractable — most of these are textbook patterns from React's you-might-not-need-an-effect docs.

Category A — Reset-on-prop-change (use key prop or derived state) — ✅ complete

When a parent prop changes and a child resets its local state to match, the React-recommended fix is for the parent to give the child a different key prop so it remounts naturally — no effect required.

Line numbers below were refreshed against dev on 2026-08-19. Several had drifted since this issue was written.

Dependency on #224. The one site already fixed here was fixed by the key={iri} remount, and #224 is scoped to revisit that remount (evaluating keepPreviousData + manual reset vs. Suspense vs. view transitions). If #224 removes the remount, PropertyDetailPanel.tsx:260 returns and Category A grows back to 7. Settle #224's direction before or alongside this category — the two are working the same mechanism from opposite ends.

Category B — Browser-API subscriptions (use useSyncExternalStore) — ✅ resolved (1 fixed, 1 removed by #154)

External stores (matchMedia, websockets) belong in useSyncExternalStore, not useEffect + setState.

Category C — Server-data-derived (overlaps with #89) — ✅ complete

State mirrored from a query response. Best fix is to derive the value via React Query's select option, or compute it in render rather than mirroring into local state.

Category D — One-shot init / event-driven UI (move to handler or useState initializer)

Work that runs once on mount or in response to a discrete event should live in a useState lazy initializer or in the handler that triggered it, not in an effect.

Recommended order

  1. Category A first (7 sites). Largest cluster, single canonical fix (key prop or derivation), and exercises the test suite hardest because the affected components have substantial coverage.
  2. Category D (4 sites). Small, mostly mechanical fixes; gets the warning count down quickly.
  3. Category B (2 sites). useSyncExternalStore migration is more involved but well-isolated.
  4. Category C (3 sites). Best done alongside the Migrate manual data fetching to React Query hooks #89 React Query migration so the changes share PRs and review context — the suggestions-page site in particular is a natural addition to issue Migrate manual data fetching to React Query hooks #89's Phase 2.

Done criteria

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions