feat: added React TanStack Router and Query cursorrules#252
feat: added React TanStack Router and Query cursorrules#252usm4nhafeez wants to merge 1 commit intoPatrickJS:mainfrom
Conversation
📝 WalkthroughWalkthroughThis 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
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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.mdcrule doc) for the Router + Query combo pattern. - Added a corresponding
rules-new/react-tanstack-router-query.mdcentry. - 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 }) => { |
There was a problem hiding this comment.
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.
| beforeLoad: ({ context }) => { | |
| beforeLoad: ({ context, location }) => { |
| mutationFn: createPost, | ||
| onSuccess: (newPost) => { | ||
| queryClient.setQueryData(postKeys.detail(newPost.id), newPost) // warm cache | ||
| queryClient.invalidateQueries({ queryKey: postKeys.list() }) |
There was a problem hiding this comment.
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.
| queryClient.invalidateQueries({ queryKey: postKeys.list() }) | |
| queryClient.invalidateQueries({ queryKey: ['posts', 'list'] }) |
| mutationFn: createPost, | ||
| onSuccess: (newPost) => { | ||
| queryClient.setQueryData(postKeys.detail(newPost.id), newPost) // warm cache | ||
| queryClient.invalidateQueries({ queryKey: postKeys.list() }) |
There was a problem hiding this comment.
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.
| queryClient.invalidateQueries({ queryKey: postKeys.list() }) | |
| queryClient.invalidateQueries({ queryKey: postKeys.lists() }) |
There was a problem hiding this comment.
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
📒 Files selected for processing (5)
README.mdrules-new/react-tanstack-router-query.mdcrules/react-tanstack-router-query-cursorrules-prompt-file/.cursorrulesrules/react-tanstack-router-query-cursorrules-prompt-file/README.mdrules/react-tanstack-router-query-cursorrules-prompt-file/react-tanstack-router-query.mdc
| 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, | ||
| } |
There was a problem hiding this comment.
🧩 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:
- 1: Query keys with object property set to undefined are not considered equal to missing property during invalidation TanStack/query#3741
- 2: Invalidation fails for query keys with null when a disabled query uses undefined in the key TanStack/query#9497
- 3: https://tanstack.com/query/latest/docs/framework/react/guides/query-invalidation
- 4: https://tanstack.com/query/v5/docs/framework/react/guides/query-invalidation
- 5: https://tanstack.com/query/v5/docs/framework/react/guides/filters
- 6: https://tanstack.com/query/v4/docs/framework/react/guides/query-keys
- 7: https://mintlify.com/tanstack/query/api/utilities
- 8: https://github.com/TanStack/query/blob/d4fd9417/packages/query-core/src/queryClient.ts
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.
| beforeLoad: ({ context }) => { | ||
| if (!context.auth.isAuthenticated) { | ||
| throw redirect({ to: '/login', search: { redirect: location.pathname } }) | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| 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, | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "react-tanstack-router-query.mdc" -type fRepository: 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 -nRepository: 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 -nRepository: 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 -nRepository: 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:
- 1: Query keys with object property set to undefined are not considered equal to missing property during invalidation TanStack/query#3741
- 2: Invalidation fails for query keys with null when a disabled query uses undefined in the key TanStack/query#9497
- 3: https://tanstack.com/query/v5/docs/framework/react/guides/query-invalidation
- 4: Partial matching of key in setQueryData TanStack/query#394
- 5: https://www.undefinedlabs.co
- 6: No information about matchQuery in the docs TanStack/query#7612
- 7: https://mintlify.com/tanstack/query/api/utilities
- 8: https://github.com/TanStack/query/blob/d4fd9417/packages/query-core/src/queryClient.ts
- 9: feat(query-core): export partialMatchKey util TanStack/query#7883
- 10: https://tessl.io/registry/tessl/npm-tanstack--query-core/5.86.0/files/docs/utilities.md
- 11: https://www.mintlify.com/tanstack/query/guides/query-keys
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.
| 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.
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