Merged
Conversation
New rule (severity: warn) flagging the §7 anti-pattern from React's
'You Might Not Need an Effect' guide:
useEffect(() => { if (card.gold) setGoldCount(c => c + 1); }, [card]);
useEffect(() => { if (goldCount > 3) setRound(r => r + 1); }, [goldCount]);
useEffect(() => { if (round > 5) setIsGameOver(true); }, [round]);
Each link adds an extra render to the tree below the component.
The chain is also rigid — setting `card` to a value from the past
re-fires every downstream effect.
Detector (per component body):
1. Collect every top-level useEffect call and, for each:
- depNames: Identifier names in the dep array
- writtenStateNames: state names whose setter is called in body
- isExternalSync: body returns cleanup OR contains a recognized
external-system call (subscribe / addEventListener / fetch /
setInterval / new MutationObserver / etc.) OR mutates a ref
2. For every ordered pair (A, B) of distinct effects:
edge iff (writes(A) ∩ deps(B)) ≠ ∅
AND ¬isExternalSync(A) AND ¬isExternalSync(B)
3. Report on every reader effect B with the chained state name.
The article calls out one legitimate 'chain' — a multi-step network
cascade where each effect re-fetches based on the previous step's
result. Those effects all have isExternalSync=true (they contain
fetch), so the rule won't fire.
Complements the existing no-cascading-set-state rule (intra-effect
multi-setter detector) without overlapping.
Tests: 5 regression cases.
flags:
- article §7 Game-style three-effect chain
does NOT flag:
- single effect with multiple setters (covered by no-cascading-set-state)
- article's GOOD network-cascade exception (each effect calls fetch)
- chat-connection effect that shares deps with state-reset effect
(createConnection().connect() / .disconnect() is external sync)
- two effects whose written/read state sets are disjoint
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
This was referenced May 7, 2026
…ternal sync Bugbot found that EXTERNAL_SYNC_MEMBER_METHOD_NAMES included `post`, `put`, `patch`, `delete`, `fetch` but omitted `get`. Read-only network calls inside an effect (`axios.get(...)`, `ky.get(...)`, `api.get(...)`) were therefore classified as internal-only sync, producing false-positive chain warnings on the article's exact 'cascade of network fetches' exception. Added `get`, `head`, and `options` to round out the standard HTTP method set. Now an `axios.get`-bearing effect correctly has `isExternalSync = true` and the rule won't flag it as a chain. Regression test mirrors the article's ShippingForm example but with `axios.get` instead of `fetch`, asserting zero hits. Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
… known client receivers
Bugbot caught: round 1 added `get` to EXTERNAL_SYNC_MEMBER_METHOD_NAMES
to support `axios.get` cascades, but `get` is also a universal
data-structure method name (`Map.get`, `URLSearchParams.get`,
`FormData.get`, `Headers.get`, `WeakMap.get`). Effects whose only
'external' call was `params.get('id')` were misclassified as external
sync, causing false-NEGATIVE chain detection.
Two new constants split the ambiguity:
EXTERNAL_SYNC_MEMBER_METHOD_NAMES — unambiguous methods (subscribe,
addEventListener, post, put, patch, delete, fetch, …). Receiver
is irrelevant; the method name alone marks the call as external.
EXTERNAL_SYNC_AMBIGUOUS_HTTP_METHOD_NAMES — the HTTP-vs-data-structure
overlapping verbs (get, head, options). Only count as external
when paired with a recognized HTTP-client receiver name.
EXTERNAL_SYNC_HTTP_CLIENT_RECEIVERS — axios, ky, got, wretch,
ofetch, api, client, http, request, fetcher.
Regression test asserts that a chain where one effect calls
`params.get('theme')` (URLSearchParams) still produces the chain
warning, verifying that data-structure-shape `.get` no longer
exempts the effect.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
#156 round 3) Bugbot caught: round-2 left `delete` in EXTERNAL_SYNC_MEMBER_METHOD_NAMES (unambiguous external-sync) while moving `get`, `head`, `options` to the ambiguous set. But `delete` has the same problem — it's the name of standard JS APIs on Map, Set, URLSearchParams, Headers, FormData, and WeakMap. An effect whose only 'external' call was `set.delete(item)` got falsely classified as external sync, silently exempting it from chain detection. Move `delete` to EXTERNAL_SYNC_AMBIGUOUS_HTTP_METHOD_NAMES so it's only counted as external when paired with a recognized HTTP-client receiver (axios, ky, …). `set.delete()`, `map.delete()`, etc. now correctly stay classified as internal-only. Regression test: a chain where one effect calls `next.delete(...)` on a Set still produces the chain warning. Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 161946b. Configure here.
…cise isExternalSync (Bugbot #156 round 4) Bugbot caught: the cascade / ShippingForm / chat tests previously had no actual write→dep state overlap between their effects, so they passed regardless of whether `isExternalSync` worked correctly. Reverting the isExternalSync exemption would NOT have caused those tests to fail — they were vacuously green. All three rewritten so the second effect depends on a state that the first effect writes: ShippingForm: writes(A) = {cities}, deps(B) = [cities] axios cascade: writes(A) = {cities}, deps(B) = [cities] chat: writes(A) = {status}, deps(B) = [status] In every case both effects are external-sync (fetch / axios.get / createConnection().connect() / addEventListener), so the chain detector exempts them. If the exemption breaks, all three tests will fail — the chain detection itself fires correctly per the existing intra-effect tests. Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
c640dfb to
d9496ef
Compare
Co-authored-by: Cursor <cursoragent@cursor.com>
d9496ef to
e5ecbb5
Compare
cursor Bot
pushed a commit
that referenced
this pull request
May 8, 2026
Rebased onto main after #154 / #155 / #156 merged. Clean reapply via patch — constants, oxlint-config, run-oxlint, fixtures, tests, and state-and-effects all applied without conflicts. Re-added USE_EFFECT_EVENT_MIN_MAJOR which got dropped in an earlier merge. All 555 tests pass. Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
aidenybai
added a commit
that referenced
this pull request
May 8, 2026
…eps, no-mirror-prop-effect, effect-needs-cleanup) (#157) Rebased onto main after #154 / #155 / #156 merged. Clean reapply via patch — all 9 files applied without conflicts. All 600 tests pass. Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
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.

New rule (severity:
warn) flagging the §7 anti-pattern from React's You Might Not Need an Effect guide.What it catches
Each link adds an extra render to the tree below the component. The chain is also rigid — setting
cardto a value from the past re-fires every downstream effect. The fix is to compute as much as possible during render (const isGameOver = round > 5) and write all related state inside the event handler that originally fires the chain.Detector
For every component body:
useEffectcall and extract:depNames: identifier names in the dep arraywrittenStateNames: state names whose setter is called insideisExternalSync: the effect returns a cleanup function or contains a recognized external-system call (subscribe,addEventListener,fetch,setInterval,new MutationObserver, etc.) or mutates a ref (ref.current = …)(A, B)of distinct effects, draw an edge iffwrites(A) ∩ deps(B) ≠ ∅and neitherAnorBisisExternalSync.Complements existing
no-cascading-set-stateno-cascading-set-statecatches multi-setter calls inside one effect.no-effect-chaincatches chains across effects. They detect orthogonal shapes and can fire independently.Article's GOOD exception is honored
The article explicitly notes that a chain of effects is appropriate when each effect synchronizes with the network (e.g. cascading dropdowns where each fetches options for the next). Each fetch-bearing effect has
isExternalSync = trueand is exempt.Tests — 5 regression cases
goldCountandroundas the chained values)no-cascading-set-state)ShippingForm(each effect callsfetch)createConnection().connect()is external sync)Plus a smoke test in
run-oxlint.test.tsagainst anEffectChainComponentfixture.Constants added
EXTERNAL_SYNC_MEMBER_METHOD_NAMES—subscribe,addEventListener,connect,fetch,post,put,patch,delete,on,watch,listen,sub,addListener,disconnect,open,closeEXTERNAL_SYNC_DIRECT_CALLEE_NAMES—fetch,ky,got,wretch,ofetch,setInterval,setTimeout,requestAnimationFrame,requestIdleCallback,queueMicrotaskEXTERNAL_SYNC_OBSERVER_CONSTRUCTORS—IntersectionObserver,MutationObserver,ResizeObserver,PerformanceObserverChecks
486/486 tests passing locally. Lint, typecheck, format clean. Changeset included (minor bump).