feat(react-doctor): add TanStack Start (14), TanStack Query (6), and Vercel Best Practices (2) rules#124
feat(react-doctor): add TanStack Start (14), TanStack Query (6), and Vercel Best Practices (2) rules#124
Conversation
…middleware, data loading, SEO, navigation, linting, and project structure rules Researched from official TanStack docs, GitHub repos (tanstack/router), ESLint plugin source, and community patterns. Covers: - File-based routing conventions and naming patterns - createServerFn patterns with validation and file organization - Middleware composition, auth patterns, context management - Data loading with route property order (critical for TS inference) - Document head management and SEO meta tags - Type-safe navigation with Link and useNavigate - ESLint plugin-router rules (property order, param names) - Project structure, app config, deployment presets Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Add 9 TanStack Start-specific rules to react-doctor, following the same pattern as NEXTJS_RULES and REACT_NATIVE_RULES: - tanstack-start-route-property-order: enforces inference-sensitive property order (params/validateSearch → beforeLoad → loader → head) - tanstack-start-no-direct-fetch-in-loader: flags raw fetch() in loaders, recommends createServerFn() for type-safe RPC - tanstack-start-server-fn-validate-input: warns when .handler() accesses data without .inputValidator() — data crosses a network boundary - tanstack-start-no-useeffect-fetch: flags fetch() inside useEffect in route files — should use route loader or createServerFn() - tanstack-start-missing-head-content: warns when __root route is missing <HeadContent /> — without it, route head() meta tags are dropped - tanstack-start-no-anchor-element: flags <a href="/..."> in route files, recommends <Link> from @tanstack/react-router - tanstack-start-server-fn-method-order: enforces correct chain order (.middleware → .inputValidator → .client → .server → .handler) - tanstack-start-no-navigate-in-render: flags navigate() during render, recommends redirect() in beforeLoad/loader - tanstack-start-no-dynamic-server-fn-import: flags dynamic import() of .functions.ts files — bundler needs static imports for RPC stubs Also adds: - tanstack-start Framework type detection via @tanstack/react-start dep - TanStack Start category and help entries in run-oxlint.ts - Removes previous .agents/skills approach (wrong location) Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…verage New rules: - tanstack-start-no-use-server-in-handler (error): flags 'use server' inside createServerFn handlers — TanStack Start handles this automatically, the directive causes compilation errors (GitHub issue #2849) - tanstack-start-no-secrets-in-loader (error): flags process.env.SECRET in loaders/beforeLoad — loaders are isomorphic and leak secrets to the client bundle (GitHub issue TanStack/cli#287) - tanstack-start-get-mutation (warn): flags GET server functions that perform mutations — should use POST to prevent CSRF and prefetch triggers - tanstack-start-redirect-in-try-catch (warn): flags throw redirect()/notFound() inside try-catch — the router catches these internally - tanstack-start-loader-parallel-fetch (warn): flags sequential awaits in loaders — should use Promise.all() to avoid waterfalls Total TanStack Start rules: 14 Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Bugs fixed: - tanstackStartRedirectInTryCatch: was using node.parent which oxlint JS plugins don't populate — rule never fired. Rewrote to use ThrowStatement visitor with tryCatchDepth tracking (matches existing nextjs pattern) - tanstackStartServerFnValidateInput: fragile chain-walking logic with potential infinite loop. Extracted shared walkServerFnChain() helper, removed unused serverFnChains variable - tanstackStartNoNavigateInRender: componentDepth never decremented for arrow components (no VariableDeclarator:exit), eventHandlerDepth was declared but never modified. Removed dead variable, simplified AGENTS.md violations fixed: - Removed unused hasDirective import - Moved SEQUENTIAL_AWAIT_THRESHOLD_FOR_LOADER to constants.ts - Moved TANSTACK_REDIRECT_FUNCTIONS to constants.ts - Moved TANSTACK_SERVER_FN_FILE_PATTERN to constants.ts - Replaced inline /^[A-Z]/ with existing UPPERCASE_PATTERN constant - Replaced duplicate MUTATION_CALLEE_NAMES with existing MUTATION_METHOD_NAMES - Removed 'as string' type cast, replaced with typeof guard Upgraded oxc: - oxlint: 1.59.0 → 1.60.0 - oxfmt: 0.44.0 → 0.45.0 Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
… cases Add tanstack-start-app fixture with: - __root.tsx missing HeadContent (triggers missing-head-content) - route-issues.tsx with 8 bad patterns (property order, direct fetch, useEffect fetch, anchor element, secrets in loader, redirect in try-catch, parallel fetch, navigate in render) - server-fn-issues.tsx with 4 bad patterns (missing validation, use-server in handler, GET mutation, dynamic import) Tests verify all rules fire with correct severity and category. Total tests: 165 → 178 (+13 new) Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Add always-on TanStack Query rules (not framework-gated — useQuery is used across all React frameworks): - query-stable-query-client (error): flags new QueryClient() inside a function body — recreates the cache on every render. Should be at module scope or wrapped in useState() - query-no-rest-destructuring (warn): flags ...rest on useQuery() result — subscribes to all fields and causes unnecessary re-renders - query-no-void-query-fn (warn): flags empty queryFn body — must return a value for the cache. Use enabled option to disable instead - query-no-query-in-effect (warn): flags refetch() inside useEffect — React Query manages refetching via queryKey and enabled - query-mutation-missing-invalidation (warn): flags useMutation without invalidateQueries — stale data may remain cached - query-no-usequery-for-mutation (warn): flags useQuery with POST/PUT/DELETE fetch — use useMutation() for write operations All 6 rules have test coverage via query-issues.tsx fixture. Total tests: 178 → 184 (+6 new) Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…update New rules sourced from vercel-labs/agent-skills react-best-practices: - js-flatmap-filter (warn): flags .map().filter(Boolean) chains — iterates the array twice and creates an intermediate array. Suggests .flatMap() for a single-pass transform+filter - rendering-script-defer-async (warn): flags <script src> without defer or async — blocks HTML parsing and delays First Contentful Paint Both rules have test fixtures and test cases. Total tests: 184 → 186 (+2 new) Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…oute, remove duplicate helper
Bugbot findings:
1. getRouteOptionsObject used arguments[1] for direct calls like
createRootRoute({...}) and createRoute({...}), but these functions
take the options object as arguments[0]. This silently disabled
route-property-order, no-direct-fetch-in-loader, no-secrets-in-loader,
and loader-parallel-fetch for any route using createRootRoute or
createRoute. Fixed to use arguments[0] for both direct and curried
call patterns.
2. getChainCalleeName was an exact duplicate of the already-exported
getCalleeName in helpers.ts. Replaced all usages with the shared
helper per AGENTS.md DRY rule.
Added createRootRoute test case to verify the fix.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
| async ({ data }: any) => { | ||
| return data; | ||
| }, | ||
| ); |
There was a problem hiding this comment.
Server function method order rule has no triggering test
Medium Severity
The wrongMethodOrder fixture only chains .handler() directly on createServerFn() — a single method with nothing to compare against. The tanstack-start-server-fn-method-order rule requires at least two order-sensitive methods (e.g., .handler().inputValidator()) to detect a violation. This fixture never triggers the rule, and the rule is entirely absent from the test suite, leaving it untested.
Reviewed by Cursor Bugbot for commit b2b5f30. Configure here.
Systematic analysis of all 22 rules found 3 bugs and 1 redundancy: 1. tanstackStartGetMutation only allowed 'POST' as a non-GET method, but PUT, PATCH, DELETE are also valid mutating methods. Now uses MUTATING_HTTP_METHODS constant to check all non-GET methods. 2. renderingScriptDeferAsync false-positived on non-executable script types like <script type="application/ld+json" src="...">. Now skips scripts with non-executable type attributes (matches the existing nextjsNoNativeScript pattern using EXECUTABLE_SCRIPT_TYPES). 3. queryStableQueryClient false-positived on non-component utility functions. Now restricts to uppercase-named functions (component convention) instead of all function scopes. Also: - Removed redundant condition in jsFlatmapFilter isFilterBoolean check - Replaced inline ["POST","PUT",...] array in queryNoUseQueryForMutation with existing MUTATING_HTTP_METHODS constant (DRY) - Removed unnecessary 'as string' type cast - Added edge-cases.tsx fixture testing false-positive freedom: correct property order, PUT/DELETE methods, validated server fns, JSON-LD scripts, deferred scripts - Added 4 false-positive-freedom tests Total tests: 186 → 190 (+4 edge case tests) Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
- Remove dead empty ArrowFunctionExpression visitor in tanstackStartNoNavigateInRender (violated 'Remove unused code') - Move STABLE_HOOK_WRAPPERS to constants.ts (was inline in tanstack-query.ts, violated 'Put all magic numbers in constants.ts') - Move SCRIPT_LOADING_ATTRIBUTES to constants.ts (was inline in performance.ts, same violation) Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Bugbot finding #3 — navigate-in-render ignores arrow components (Medium): Rewrote tanstackStartNoNavigateInRender to drop componentDepth tracking entirely. Since the rule is already scoped to /routes/ files, it now flags any navigate() call that isn't inside useEffect or a JSX event handler (onClick, onChange, etc.). This correctly catches arrow function components like const MyPage = () => { navigate(...) }. Bugbot finding #4 — server-fn-method-order has no triggering test (Medium): The wrongMethodOrder fixture only had .handler() (single method, nothing to compare). Changed to .handler().inputValidator() which has two order-sensitive methods in the wrong order. Added test case. Also simplified the createServerFn mock to use a Proxy for chainability. Total tests: 190 → 192 (+2 previously untested rules now covered) Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…QueryClient
Bugbot finding: componentDepth was incremented for arrow function
components (const App = () => {...}) but never decremented — no
VariableDeclarator:exit handler. This caused componentDepth to leak,
so any new QueryClient() at module scope after an arrow component
would be falsely flagged.
Added the exit handler matching the enter condition, same pattern
used by architecture.ts for component tracking.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…der duplicates Bugbot finding: tanstackStartRedirectInTryCatch flagged throw redirect() inside catch blocks, but that's safe — a throw inside catch propagates normally and isn't re-caught. The rule's own message recommended 're-throw in the catch' which would trigger the same warning. Fixed by tracking CatchClause depth separately and skipping reports inside catch. Bugbot finding: tanstackStartServerFnMethodOrder fired on every CallExpression in a chain, producing duplicate diagnostics for 3+ method chains. Fixed by only reporting from the outermost call (where the last collected method matches the current node's method). Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…jsCombineIterations Bugbot: isFilterBoolean matched any single-param arrow whose body was any Identifier (e.g. .filter(x => someExternalVar)), not just identity functions like .filter(x => x). Fixed by verifying the body identifier name matches the parameter name. Bugbot: .map().filter(Boolean) triggered both jsFlatmapFilter and jsCombineIterations. Added early return in jsCombineIterations to skip .map().filter() pairs — the more specific jsFlatmapFilter handles those with a better suggestion (.flatMap instead of generic reduce). 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 2 potential issues.
There are 3 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 40e65d2. Configure here.
| const innerMethod = innerCall.callee.property.name; | ||
| if (!CHAINABLE_ITERATION_METHODS.has(innerMethod)) return; | ||
|
|
||
| if (innerMethod === "map" && outerMethod === "filter") return; |
There was a problem hiding this comment.
Overly broad exclusion drops .map().filter() lint coverage
Medium Severity
The new exclusion if (innerMethod === "map" && outerMethod === "filter") return in jsCombineIterations skips all .map().filter() chains, but the new jsFlatmapFilter rule only catches the subset .map().filter(Boolean) (or identity arrow). This creates a regression where .map(transform).filter(customPredicate) is no longer flagged by either rule, losing previously-existing lint coverage. The exclusion needs to be narrowed to only skip when the filter argument is Boolean or an identity function.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 40e65d2. Configure here.
| } | ||
| }, | ||
| }), | ||
| }; |
There was a problem hiding this comment.
False positive on type="module" scripts (deferred by default)
Medium Severity
The renderingScriptDeferAsync rule treats type="module" scripts as executable (via EXECUTABLE_SCRIPT_TYPES) and requires an explicit defer or async attribute. However, per the HTML spec, module scripts are deferred by default — the defer attribute has no effect on them. A <script type="module" src="..."> without defer or async will be incorrectly flagged, producing a false positive.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 40e65d2. Configure here.


What
Adds 22 new rules to react-doctor: 14 TanStack Start + 6 TanStack Query + 2 Vercel Best Practices, all with tests and edge case validation. Also upgrades oxc tooling and fixes 7 bugs found during review.
@tanstack/react-startis detectedOxc Upgrades
oxlint: 1.59.0 → 1.60.0oxfmt: 0.44.0 → 0.45.0All Bugs Fixed
tanstackStartRedirectInTryCatchnode.parentwhich oxlint JS plugins don't populate — rule never firedtanstackStartServerFnValidateInputgetRouteOptionsObject[1]instead of[0]) disabled 4 rules forcreateRootRoute/createRoutetanstackStartNoNavigateInRendercomponentDepthleaked for arrow components; deadeventHandlerDepthvariabletanstackStartGetMutationrenderingScriptDeferAsync<script type="application/ld+json">queryStableQueryClientValidation
Rules
See individual rule examples in previous PR description revision — all 22 rules are documented with bad/good code examples.