refactor: remove the set-state-in-effect suppressions#117
Merged
aaronjmars merged 1 commit intoJul 20, 2026
Conversation
All three were disabled with rationale rather than fixed. Each had a real fix that also simplifies the code. ColumnCard opened its search row from two effects. Opening is a state adjustment, so it now happens during render — React re-runs the component before paint, with no cascading commit and no frame where the row is missing. The query-driven open has to stay edge-triggered. Both the "Esc" button and the toolbar toggle close the row without clearing the query, so a plain `searchActive && !searchOpen` test re-opens it on the next render and makes the row impossible to dismiss. Comparing against the previous value keeps it a transition; the mount case is covered by seeding `searchOpen` from `searchActive` instead. The effect keeps the two jobs that belong in one: moving focus and draining the one-shot signal. VersionHistoryDialog stays mounted for the lifetime of the sidebar, which is the only reason it needed to set a loading flag synchronously on open. Split the list into a child mounted per-open and keyed by deck, so `useState`'s initial value is the reset. `snapshots: null` then encodes "not loaded yet" and the separate `loading` boolean goes away, along with a one-render flash of the empty-state copy that the old `loading: false` default allowed. Verified against a live deck: closing search with an active query stays closed, the query survives close/reopen, clearing it mid-type keeps the row up, `/` opens and focuses, and the dialog reloads cleanly on reopen. No React warnings in the console.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #116, which disabled three
react-hooks/set-state-in-effectreports with rationale rather than fixing them. Each had a real fix that also removes state.ColumnCard search row
Two effects existed only to open the row. Opening is a state adjustment, so it now happens during render — React re-runs the component immediately, before paint, with no cascading commit.
The query-driven open has to stay edge-triggered, and this is the part worth reviewing. The obvious version —
— is wrong. Both the "Esc" button and the toolbar toggle close the row without clearing the query, so that test re-opens it on the very next render and the row becomes impossible to dismiss. I wrote that version first and caught it reading the close handlers. Comparing against the previous value keeps it a transition, which is what the old
[searchActive]dep array was really encoding.The mount case (a query that survived a collapse/expand or tab switch) moves to seeding
searchOpenfromsearchActive, which also removes a one-frame flicker. The effect is left with the two jobs that genuinely belong in one: moving DOM focus and draining the one-shot/signal.VersionHistoryDialog
The dialog stays mounted for the lifetime of the sidebar — that was the only reason it had to set a loading flag synchronously on open. Splitting the list into a child that mounts per-open and is keyed by deck makes
useState's initial value the reset.snapshots: DeckSnapshotMeta[] | nullthen encodes "not loaded yet" directly, so the separateloadingboolean disappears — along with a one-render flash of the empty-state copy that the oldloading: falsedefault allowed on first open.Verification
There is no test coverage for any of this (the 279 tests don't touch either file), and reasoning alone already produced one bug here, so I exercised it against a live deck in a browser:
nvidiaretained ✅/shortcutConsole: 0 React errors, 0 warnings — no "cannot update while rendering", no render loop.
eslint --max-warnings=0clean · 279/279 tests ·tsc --noEmit·next buildall pass. Noset-state-in-effectdisables remain in the codebase.