Skip to content

feat: added React TanStack Router and Query cursorrules#252

Open
usm4nhafeez wants to merge 1 commit intoPatrickJS:mainfrom
usm4nhafeez:add/react-tanstack-router-query-cursorrules
Open

feat: added React TanStack Router and Query cursorrules#252
usm4nhafeez wants to merge 1 commit intoPatrickJS:mainfrom
usm4nhafeez:add/react-tanstack-router-query-cursorrules

Conversation

@usm4nhafeez
Copy link
Copy Markdown

@usm4nhafeez usm4nhafeez commented Apr 13, 2026

Added rules for the Router + Query combo pattern in React SPAs — loader-to-cache integration, zero loading spinners, type-safe search params driving query keys, and mutation patterns. Distinct from the individual Router or Query rules.

Summary by CodeRabbit

  • Documentation
    • Added comprehensive guides for integrating TanStack Router v1 with TanStack Query v5 in React applications, covering setup patterns, query management strategies, cache warming via route loaders, mutation workflows, search parameter handling, and performance optimization techniques.

Copilot AI review requested due to automatic review settings April 13, 2026 08:30
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 13, 2026

📝 Walkthrough

Walkthrough

This pull request adds comprehensive documentation for integrating TanStack Router v1 and TanStack Query v5 in React SPAs, including setup patterns, query-factory conventions, loader-driven cache prefetching, search-params handling, mutation workflows, and DevTools configuration across multiple documentation files.

Changes

Cohort / File(s) Summary
Documentation Index
README.md
Added listing entry under State Management for the new TanStack Router + Query rules with link and description.
Rule Documentation
rules/react-tanstack-router-query-cursorrules-prompt-file/.cursorrules, rules/react-tanstack-router-query-cursorrules-prompt-file/README.md, rules/react-tanstack-router-query-cursorrules-prompt-file/react-tanstack-router-query.mdc, rules-new/react-tanstack-router-query.mdc
New documentation files defining architecture, setup patterns, queryOptions factories, loader-based cache warming, search-params-driven queries, mutation workflows with cache invalidation, hover prefetching, and key behavioral rules for TanStack Router v1 + Query v5 integration.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Suggested reviewers

  • PatrickJS

Poem

🐰 A new rule has hopped into our warren,
TanStack Router and Query in harmonious accord,
No spinners spinning, just loaders prefetching with care,
Type-safe routes and cached queries everywhere!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding new cursor rules documentation for React TanStack Router and Query integration.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new Cursor ruleset describing an integrated pattern for React SPAs using TanStack Router v1 + TanStack Query v5, focusing on loader-to-query-cache prefetching, type-safe search params, and mutation/cache workflows to minimize route-level loading states.

Changes:

  • Added a new rules prompt-file package (README, .cursorrules, and an .mdc rule doc) for the Router + Query combo pattern.
  • Added a corresponding rules-new/react-tanstack-router-query.mdc entry.
  • Linked the new ruleset from the repository root README.md.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
rules/react-tanstack-router-query-cursorrules-prompt-file/README.md Introduces the new Router+Query combo ruleset and what it covers.
rules/react-tanstack-router-query-cursorrules-prompt-file/react-tanstack-router-query.mdc Provides the concise .mdc rules content and example patterns.
rules/react-tanstack-router-query-cursorrules-prompt-file/.cursorrules Full Cursor rules content with detailed examples (setup, loaders, mutations, auth, devtools).
rules-new/react-tanstack-router-query.mdc Duplicates the .mdc rules content in the rules-new/ format.
README.md Adds a link to the new ruleset in the main index.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


// src/routes/_auth.tsx (pathless layout for protected routes)
export const Route = createFileRoute('/_auth')({
beforeLoad: ({ context }) => {
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the beforeLoad example, location is referenced (location.pathname) but it isn’t in scope in this function signature, so this snippet won’t compile/run as written. Destructure location from the beforeLoad args (or use the router-provided location from the args) before using pathname in the redirect search param.

Suggested change
beforeLoad: ({ context }) => {
beforeLoad: ({ context, location }) => {

Copilot uses AI. Check for mistakes.
mutationFn: createPost,
onSuccess: (newPost) => {
queryClient.setQueryData(postKeys.detail(newPost.id), newPost) // warm cache
queryClient.invalidateQueries({ queryKey: postKeys.list() })
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

postKeys.list includes the optional filters value as the 3rd element of the query key, so calling postKeys.list() in invalidateQueries produces ['posts','list', undefined] and will not invalidate list queries that were created with real filter objects (e.g. ['posts','list',{...}]). Introduce a prefix key (e.g. lists(): ['posts','list']) and invalidate using that prefix, or invalidate via a higher-level prefix like postKeys.all.

Suggested change
queryClient.invalidateQueries({ queryKey: postKeys.list() })
queryClient.invalidateQueries({ queryKey: ['posts', 'list'] })

Copilot uses AI. Check for mistakes.
mutationFn: createPost,
onSuccess: (newPost) => {
queryClient.setQueryData(postKeys.detail(newPost.id), newPost) // warm cache
queryClient.invalidateQueries({ queryKey: postKeys.list() })
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as the prompt-file version: postKeys.list() includes an explicit undefined segment in the query key, so invalidateQueries({ queryKey: postKeys.list() }) won’t invalidate list queries created with filter objects. Use a stable prefix key (e.g. postKeys.lists() / postKeys.all) for invalidation.

Suggested change
queryClient.invalidateQueries({ queryKey: postKeys.list() })
queryClient.invalidateQueries({ queryKey: postKeys.lists() })

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@rules-new/react-tanstack-router-query.mdc`:
- Around line 43-47: postKeys.list currently embeds the filter object in the
query key which prevents broad invalidation; add a new factory
postKeys.listPrefix = () => [...postKeys.all, 'list'] as const and change
postKeys.list to return [...postKeys.listPrefix(), f] as const, then update any
invalidation calls to use invalidateQueries({ queryKey: postKeys.listPrefix() })
so all filtered and unfiltered list queries are matched and invalidated.

In `@rules/react-tanstack-router-query-cursorrules-prompt-file/.cursorrules`:
- Around line 224-227: The beforeLoad hook uses location.pathname which is out
of scope; update the beforeLoad signature to destructure the location (e.g.,
beforeLoad: ({ context, location }) => { ... }) or otherwise obtain the location
from the available params, then pass location.pathname into redirect; ensure the
redirect call (redirect({ to: '/login', search: { redirect: location.pathname }
})) uses the in-scope location variable and still checks
context.auth.isAuthenticated before throwing.

In
`@rules/react-tanstack-router-query-cursorrules-prompt-file/react-tanstack-router-query.mdc`:
- Around line 43-47: postKeys.list currently includes the optional filters
parameter directly in the tuple, so calling postKeys.list() produces
['posts','list', undefined] which won't structurally match ['posts','list',
{page:1}]; change the factory to return a shorter tuple when no filters are
provided and include the filters only when present (e.g. in postKeys.list ensure
it returns [...postKeys.all,'list'] when f is undefined and
[...postKeys.all,'list', f] when f is provided) so invalidation by
postKeys.list() correctly matches unfiltered list keys and filtered lists remain
addressable.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 85a05ff5-c52c-40fd-87bd-d661c23ea4d5

📥 Commits

Reviewing files that changed from the base of the PR and between fc2ce04 and 6c9d70e.

📒 Files selected for processing (5)
  • README.md
  • rules-new/react-tanstack-router-query.mdc
  • rules/react-tanstack-router-query-cursorrules-prompt-file/.cursorrules
  • rules/react-tanstack-router-query-cursorrules-prompt-file/README.md
  • rules/react-tanstack-router-query-cursorrules-prompt-file/react-tanstack-router-query.mdc

Comment on lines +43 to +47
export const postKeys = {
all: ['posts'] as const,
detail: (id: string) => [...postKeys.all, 'detail', id] as const,
list: (f?: PostFilters) => [...postKeys.all, 'list', f] as const,
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check the file exists and read the relevant sections
cat -n rules-new/react-tanstack-router-query.mdc | sed -n '40,100p'

Repository: PatrickJS/awesome-cursorrules

Length of output: 2327


🌐 Web query:

TanStack Query invalidateQueries queryKey matching behavior undefined filters

💡 Result:

TanStack Query's invalidateQueries uses partial prefix matching by default for queryKey filters, implemented via partialMatchKey and matchQuery utilities. For arrays, the filter key must be a strict prefix of the query key. For objects in keys, matching iterates over Object.keys of the filter object, checking if those properties exist in the query key object (regardless of value). Key behavior with undefined filters: - When hashing query keys for caching (via JSON.stringify-like hashKey), undefined object properties are omitted, so {filter: undefined} hashes the same as {}. - However, invalidateQueries matching compares the raw queryKey structures without hashing. A filter with explicit {filter: undefined} only matches queries where the 'filter' property is present (even if undefined there), but does not prefix-match queries lacking the 'filter' property entirely. This leads to unintuitive behavior: invalidateQueries([{..., filter: undefined}]) matches cached queries from [{...}] (no filter) in some cases but not consistently, as noted in open GitHub issue #3741. Workarounds: - Avoid undefined in query keys; use null, {}, or omit the property. - Use query key factories that conditionally include properties only when defined. - For explicit no-filter invalidation, use a shorter prefix key or predicate function. - Custom queryKeyHashFn can normalize undefined to null globally, but doesn't affect raw matching. Official docs recommend including all dependencies in keys but don't explicitly address undefined handling beyond hashing notes. Behavior unchanged in v5 as of 2026.

Citations:


List-query invalidation pattern is too narrow and can miss filtered caches.

postKeys.list(filters) includes the filter object in the key, creating queries like ['posts', 'list', {page: 1}]. Calling invalidateQueries({ queryKey: postKeys.list() }) creates a filter key ['posts', 'list', undefined], which only matches queries with undefined as the third element—not filtered variants. This leaves stale list data after mutations. Use a separate prefix factory for broad invalidation:

Suggested fix
 export const postKeys = {
   all: ['posts'] as const,
+  lists: () => [...postKeys.all, 'list'] as const,
   detail: (id: string) => [...postKeys.all, 'detail', id] as const,
-  list: (f?: PostFilters) => [...postKeys.all, 'list', f] as const,
+  list: (f?: PostFilters) => [...postKeys.lists(), f] as const,
 }

Then at line 95, change:

-    queryClient.invalidateQueries({ queryKey: postKeys.list() })
+    queryClient.invalidateQueries({ queryKey: postKeys.lists() })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rules-new/react-tanstack-router-query.mdc` around lines 43 - 47,
postKeys.list currently embeds the filter object in the query key which prevents
broad invalidation; add a new factory postKeys.listPrefix = () =>
[...postKeys.all, 'list'] as const and change postKeys.list to return
[...postKeys.listPrefix(), f] as const, then update any invalidation calls to
use invalidateQueries({ queryKey: postKeys.listPrefix() }) so all filtered and
unfiltered list queries are matched and invalidated.

Comment on lines +224 to +227
beforeLoad: ({ context }) => {
if (!context.auth.isAuthenticated) {
throw redirect({ to: '/login', search: { redirect: location.pathname } })
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Auth guard example references location out of scope.

beforeLoad only destructures context, but uses location.pathname. This will fail if copied as-is.

Suggested fix
-export const Route = createFileRoute('/_auth')({
-  beforeLoad: ({ context }) => {
+export const Route = createFileRoute('/_auth')({
+  beforeLoad: ({ context, location }) => {
     if (!context.auth.isAuthenticated) {
       throw redirect({ to: '/login', search: { redirect: location.pathname } })
     }
   },
 })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
beforeLoad: ({ context }) => {
if (!context.auth.isAuthenticated) {
throw redirect({ to: '/login', search: { redirect: location.pathname } })
}
beforeLoad: ({ context, location }) => {
if (!context.auth.isAuthenticated) {
throw redirect({ to: '/login', search: { redirect: location.pathname } })
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rules/react-tanstack-router-query-cursorrules-prompt-file/.cursorrules`
around lines 224 - 227, The beforeLoad hook uses location.pathname which is out
of scope; update the beforeLoad signature to destructure the location (e.g.,
beforeLoad: ({ context, location }) => { ... }) or otherwise obtain the location
from the available params, then pass location.pathname into redirect; ensure the
redirect call (redirect({ to: '/login', search: { redirect: location.pathname }
})) uses the in-scope location variable and still checks
context.auth.isAuthenticated before throwing.

Comment on lines +43 to +47
export const postKeys = {
all: ['posts'] as const,
detail: (id: string) => [...postKeys.all, 'detail', id] as const,
list: (f?: PostFilters) => [...postKeys.all, 'list', f] as const,
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "react-tanstack-router-query.mdc" -type f

Repository: PatrickJS/awesome-cursorrules

Length of output: 208


🏁 Script executed:

wc -l "./rules/react-tanstack-router-query-cursorrules-prompt-file/react-tanstack-router-query.mdc"

Repository: PatrickJS/awesome-cursorrules

Length of output: 168


🏁 Script executed:

sed -n '40,100p' "./rules/react-tanstack-router-query-cursorrules-prompt-file/react-tanstack-router-query.mdc" | cat -n

Repository: PatrickJS/awesome-cursorrules

Length of output: 2327


🏁 Script executed:

sed -n '50,100p' "./rules/react-tanstack-router-query-cursorrules-prompt-file/react-tanstack-router-query.mdc" | cat -n

Repository: PatrickJS/awesome-cursorrules

Length of output: 1966


🏁 Script executed:

sed -n '43,50p' "./rules/react-tanstack-router-query-cursorrules-prompt-file/react-tanstack-router-query.mdc" | cat -n

Repository: PatrickJS/awesome-cursorrules

Length of output: 448


🏁 Script executed:

grep -n "invalidateQueries" "./rules/react-tanstack-router-query-cursorrules-prompt-file/react-tanstack-router-query.mdc"

Repository: PatrickJS/awesome-cursorrules

Length of output: 224


🌐 Web query:

TanStack Query invalidateQueries queryKey matching behavior undefined partial match

💡 Result:

TanStack Query's invalidateQueries uses partial prefix matching on query keys by default (exact: false). A filter queryKey matches a cached queryKey if the filter is a prefix of the cached key at every level, including array indices and object keys. For undefined values in object query key elements: - During hashing (for storage), JSON.stringify omits properties with undefined values, so ['todos', {filter: undefined}] hashes the same as ['todos']. - During matching (for invalidateQueries etc.), comparison uses structural equality on the full queryKey objects without hashing. Object.keys iteration means a key with {filter: undefined} has a 'filter' property present (value undefined), so it only prefix-matches keys that also have 'filter' defined. It does NOT match keys missing the 'filter' property entirely, even though they hash the same. Example from GitHub issue #3741: - Cached: ['entity', 'todos', 'list'] and ['entity', 'todos', 'list', {filter: undefined}] - invalidateQueries({ queryKey: ['entity', 'todos', 'list', {filter: undefined}] }) invalidates the second but NOT the first. Workarounds: - Use null instead of undefined: {filter: null ?? undefined} → {filter: null}, which is explicit. - Custom queryKeyHashFn to serialize undefined as null. - Strip undefined properties from keys before use. - Use predicate function for custom matching logic. This behavior is intentional and documented indirectly via query filters (exact, queryKey) and query keys guide. Use exact: true for precise matching. Utility functions like partialMatchKey and matchQuery (exported in recent versions) implement this logic and can be used in predicates.

Citations:


Query-key factory design can cause incomplete invalidation after mutations.

When postKeys.list(filters) includes optional filters directly in the key, invalidating with postKeys.list() creates a key with undefined that doesn't structurally match keys like ['posts', 'list', {page: 1}]. This leaves filtered list queries stale in the cache.

Suggested fix
 export const postKeys = {
   all: ['posts'] as const,
+  lists: () => [...postKeys.all, 'list'] as const,
   detail: (id: string) => [...postKeys.all, 'detail', id] as const,
-  list: (f?: PostFilters) => [...postKeys.all, 'list', f] as const,
+  list: (f?: PostFilters) => [...postKeys.lists(), f] as const,
 }

At line 95:

-    queryClient.invalidateQueries({ queryKey: postKeys.list() })
+    queryClient.invalidateQueries({ queryKey: postKeys.lists() })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const postKeys = {
all: ['posts'] as const,
detail: (id: string) => [...postKeys.all, 'detail', id] as const,
list: (f?: PostFilters) => [...postKeys.all, 'list', f] as const,
}
export const postKeys = {
all: ['posts'] as const,
lists: () => [...postKeys.all, 'list'] as const,
detail: (id: string) => [...postKeys.all, 'detail', id] as const,
list: (f?: PostFilters) => [...postKeys.lists(), f] as const,
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@rules/react-tanstack-router-query-cursorrules-prompt-file/react-tanstack-router-query.mdc`
around lines 43 - 47, postKeys.list currently includes the optional filters
parameter directly in the tuple, so calling postKeys.list() produces
['posts','list', undefined] which won't structurally match ['posts','list',
{page:1}]; change the factory to return a shorter tuple when no filters are
provided and include the filters only when present (e.g. in postKeys.list ensure
it returns [...postKeys.all,'list'] when f is undefined and
[...postKeys.all,'list', f] when f is provided) so invalidation by
postKeys.list() correctly matches unfiltered list keys and filtered lists remain
addressable.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants