feat(jotai): add three Jotai rules + register jotai bucket#531
Merged
Conversation
aidenybai
added a commit
that referenced
this pull request
May 27, 2026
…rmost method Bugbot caught that walking inward past non-matching methods in freshFromMethodChain produced false positives on chains like `get(users).filter(fn).reduce(sum, 0)` — the outer `.reduce()` returns a primitive that dedupes via Object.is correctly, but the inward walk would find `.filter()` and flag the whole chain. Only the OUTERMOST method decides whether the atom's value is a fresh structure. Removes the inward walk; tightens the detector to the documented contract. Adds three regression tests covering the false-positive shapes the bot called out: `.filter().reduce()`, `.find()/.some()/.includes()`, and `.join()`. Bugbot: #531 (comment)
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 2dd1069. Configure here.
Three Jotai re-render correctness rules from peterp.me/articles/jotai-structural-sharing-vs-selectatom/. Peter measured 44× more commits than the equivalent React Query consumer when these patterns appear in jotai-tanstack-query codebases. - `jotai-select-atom-in-render-body` (error severity): `selectAtom` called in a component / hook body without `useMemo` returns a NEW atom on every render. `useAtomValue` re-subscribes to that new atom, triggering an infinite render loop — jotai's own #1 documented footgun. Skips module scope, `useMemo` / `useCallback` callbacks, non-jotai `selectAtom` imports, and lowercase non-hook helpers. - `jotai-derived-atom-returns-fresh-object`: jotai propagates with Object.is; a derivation that returns a fresh ObjectExpression / ArrayExpression literal — or an array-producing method chain (.filter, .map, .toSorted, Object.entries(...), Array.from(...)) — fails Object.is on every notify and re-renders every consumer. Handles concise arrow bodies (ParenthesizedExpression stripped), block bodies with multiple top-level returns (every return must produce a fresh structure), renamed `get` parameters. Skips primitive returns, `get(x).foo` member chains, write-only atoms, constant atoms, conditional-mix returns. - `jotai-tq-use-raw-query-atom`: subscribing to `atomWithQuery` / `atomWithSuspenseQuery` / `atomWithInfiniteQuery` directly with `useAtomValue` puts every consumer on the full `QueryObserverResult` broadcast path. TanStack rebuilds that envelope on every observer notify; consumers re-render on every refetch, focus event, and no-op cache hit. Recommends deriving the field first: `atom((g) => g(queryAtom).data)`. Tracks file-local `atomWithQuery(...)` bindings, aliased factory imports, and cross-file imports via the established `*QueryAtom` / `*SuspenseQueryAtom` / `*InfiniteQueryAtom` naming convention. Registers the `jotai/` bucket in `scripts/generate-rule-registry.mjs` with category "State & Effects". Rules self-gate via import detection (no new project-capability needed).
…rmost method Bugbot caught that walking inward past non-matching methods in freshFromMethodChain produced false positives on chains like `get(users).filter(fn).reduce(sum, 0)` — the outer `.reduce()` returns a primitive that dedupes via Object.is correctly, but the inward walk would find `.filter()` and flag the whole chain. Only the OUTERMOST method decides whether the atom's value is a fresh structure. Removes the inward walk; tightens the detector to the documented contract. Adds three regression tests covering the false-positive shapes the bot called out: `.filter().reduce()`, `.find()/.some()/.includes()`, and `.join()`. Bugbot: #531 (comment)
2dd1069 to
339340a
Compare
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.

Why
Catches three Jotai re-render footguns from peterp.me/articles/jotai-structural-sharing-vs-selectatom. Peter measured 44× more commits than the equivalent React Query consumer when these patterns appear in jotai-tanstack-query codebases — same query, same response, same render output, just a lot more React work.
jotai-select-atom-in-render-body(error severity)selectAtomreturns a new atom on every call. Calling it in a component / hook body withoutuseMemorebuilds the derived atom on every render →useAtomValuere-subscribes to a brand-new atom each render → infinite render loop. Jotai's own documented #1 footgun.Before:
After:
jotai-derived-atom-returns-fresh-objectJotai propagates derived values with
Object.is. A derivation that returns a freshObjectExpression/ArrayExpressionliteral — OR an array-producing method chain (.filter(),.map(),.toSorted(),Object.entries(...),Array.from(...)) — failsObject.ison every notify and re-renders every consumer, even when the field values didn't change.Before:
After:
jotai-tq-use-raw-query-atomSubscribing directly to
atomWithQueryputs every consumer on the fullQueryObserverResultbroadcast path. TanStack rebuilds that envelope on every observer notify (refetches, focus events, no-op cache hits) — consumers re-render even when their field didn't change.Before:
After:
What changed
Eval results
RDE not run — local runner is `git clone` + `npm install` bound (sequential, no Vercel sandbox credentials locally). Jotai usage in the existing OSS corpus is small (~2 hits in the manifest), so signal would be low even with a full RDE run. 45 co-located tests cover the documented bug shapes, every v1 valid case, aliased imports, renamed parameters, and the cross-file naming-convention path.
Test plan
Note
Low Risk
Additive lint-only rules gated on Jotai imports; no runtime or auth changes, with bounded false-positive risk on the cross-file
*QueryAtomnaming heuristic.Overview
Adds a
jotairule bucket (category State & Effects) and three import-gated oxlint rules for common Jotai re-render footguns.jotai-select-atom-in-render-body(error) flagsselectAtomfromjotai/jotai/utilsinside a component oruse*hook body unless it sits in auseMemo/useCallbackcallback or at module scope—avoids rebuilding a new derived atom every render and infinite re-subscribe loops.jotai-derived-atom-returns-fresh-object(warn) flags read-onlyatom((get) => …)derivations that return fresh object/array literals or outermost “allocating” chains (e.g..map,Object.entries) when the body actually callsget, since Jotai dedupes withObject.is.jotai-tq-use-raw-query-atom(warn) flagsuseAtom/useAtomValueonjotai-tanstack-queryquery atoms (file-local factory tracking plus cross-file*QueryAtomnaming), and nudges subscribing via a field-derived atom instead of the full observer envelope.Registry codegen and
rule-registry.tswire all three in; 45 co-located tests cover positive/negative cases, aliases, and edge paths.Reviewed by Cursor Bugbot for commit 339340a. Bugbot is set up for automated code reviews on this repo. Configure here.