Skip to content

perf(platform): simplify TanStack Query#464

Merged
yannickmonney merged 2 commits into
mainfrom
feat/simplify-tanstack-query
Feb 14, 2026
Merged

perf(platform): simplify TanStack Query#464
yannickmonney merged 2 commits into
mainfrom
feat/simplify-tanstack-query

Conversation

@yannickmonney

@yannickmonney yannickmonney commented Feb 14, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

Release Notes

  • Refactor

    • Simplified mutation handling across features by removing optimistic update patterns; operations now use standard mutations without immediate preview updates.
    • Removed automatic cache invalidation from mutations and actions throughout the platform.
  • New Features

    • Added pagination support for website listing with status filtering.
    • Enhanced route-level data prefetching to improve initial page load performance.
  • Tests

    • Updated tests to reflect simplified mutation handling.
    • Removed deprecated optimistic mutation test suites.

@greptile-apps

greptile-apps Bot commented Feb 14, 2026

Copy link
Copy Markdown

Greptile Overview

Greptile Summary

This PR significantly simplifies the TanStack Query integration by removing custom invalidation logic and optimistic updates in favor of Convex's built-in reactivity.

Key Changes:

  • Removed custom invalidates arrays from all mutation hooks across features (automations, custom agents, documents, teams, vendors, etc.)
  • Deleted useConvexOptimisticMutation hook and all optimistic update logic (~187 lines removed)
  • Deleted manual invalidateConvexQueries helper function
  • Simplified useConvexMutation and useConvexAction to delegate directly to @convex-dev/react-query hooks
  • Removed explicit staleTime configuration from router to rely on Convex reactivity
  • Added route loaders with prefetchQuery for better initial data loading
  • Introduced pagination support for websites list with smart index selection

Architecture Improvement:
The refactor leverages Convex's native WebSocket-based reactivity, where mutations automatically trigger query updates through the Convex backend. This eliminates the need for manual cache invalidation and reduces ~1,788 lines of boilerplate code while maintaining the same user-facing behavior.

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • The refactoring is well-executed with comprehensive test coverage updates, follows best practices by delegating to official Convex integrations, and removes significant complexity while maintaining functionality through Convex's built-in reactivity system
  • No files require special attention

Important Files Changed

Filename Overview
services/platform/app/hooks/use-convex-mutation.ts Simplified to delegate to @convex-dev/react-query instead of manual invalidation
services/platform/app/hooks/use-convex-action.ts Simplified to delegate to @convex-dev/react-query instead of manual invalidation
services/platform/app/hooks/use-convex-optimistic-mutation.ts Removed optimistic mutations file; functionality replaced by Convex native reactivity
services/platform/app/hooks/invalidate.ts Removed manual invalidation helpers; now handled by Convex reactivity
services/platform/app/router.tsx Removed explicit staleTime config to leverage Convex reactivity
services/platform/app/features/websites/components/websites-table.tsx Updated to use UsePaginatedQueryResult instead of static array
services/platform/convex/websites/list_websites_paginated.ts New paginated query helper with smart index selection for filters

Flowchart

flowchart TB
    subgraph Before["Before: Manual Invalidation"]
        A1[Component calls mutation hook]
        A2[useConvexMutation with invalidates array]
        A3[Execute Convex mutation]
        A4[onSettled: Manually invalidate queries]
        A5[Query cache invalidated]
        A6[Component re-fetches data]
        A1 --> A2 --> A3 --> A4 --> A5 --> A6
    end
    
    subgraph After["After: Convex Reactivity"]
        B1[Component calls mutation hook]
        B2[useConvexMutation delegates to @convex-dev/react-query]
        B3[Execute Convex mutation]
        B4[Convex WebSocket pushes updates]
        B5[React Query cache auto-updates]
        B6[Component re-renders with fresh data]
        B1 --> B2 --> B3 --> B4 --> B5 --> B6
    end
    
    style Before fill:#ffebee
    style After fill:#e8f5e9
Loading

Last reviewed commit: 38ea96c

@coderabbitai

coderabbitai Bot commented Feb 14, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This pull request removes optimistic mutation handling and explicit cache invalidation from the platform's Convex hooks infrastructure. The refactoring deletes useConvexOptimisticMutation and its utilities, replacing all usages with standard useConvexMutation, simplifies function signatures by removing parameters like workflowRootId and customAgentId, and removes invalidates options from action and mutation calls. Additionally, a new paginated websites query is introduced with filtering support, route loaders are added across multiple pages for data prefetching, and related tests are updated to reflect these changes.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'perf(platform): simplify TanStack Query' clearly summarizes the main change across the entire changeset, which involves removing optimistic mutation patterns and simplifying the TanStack Query integration.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/simplify-tanstack-query

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.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
services/platform/app/features/conversations/hooks/__tests__/mutation-hooks.test.ts (1)

30-34: 🧹 Nitpick | 🔵 Trivial

Dead mock: queries block is no longer referenced.

Since cache invalidation was removed, no test in this file references api.conversations.queries. Consider removing this leftover to keep the mock minimal.

🧹 Suggested cleanup
     conversations: {
       mutations: {
         closeConversation: 'closeConversation',
         reopenConversation: 'reopenConversation',
         markConversationAsRead: 'markConversationAsRead',
         markConversationAsSpam: 'markConversationAsSpam',
       },
-      queries: {
-        listConversations: 'listConversations',
-      },
     },
services/platform/app/routes/dashboard/$id/_knowledge/websites.tsx (1)

9-12: 🧹 Nitpick | 🔵 Trivial

query field in search schema is declared but unused in the component.

searchSchema defines query: z.string().optional() but WebsitesPage only reads search.status. If query is used by the client-side search within DataTable, it may not need to be in the URL schema. Verify whether this is intentional for URL-persisted search state or leftover from a previous iteration.

🤖 Fix all issues with AI agents
In `@services/platform/app/hooks/__tests__/use-convex-action.test.ts`:
- Around line 56-60: The test incorrectly asserts referential equality of the
mutationFn returned by useConvexAction to mockActionFn even though
useConvexAction sets mutationFn to a wrapper arrow function; update the
assertion to verify behavioral equivalence instead: grab options =
mockUseMutation.mock.calls[0]?.[0], ensure options.mutationFn is a function,
call options.mutationFn with a sample payload and then assert that mockActionFn
was called with that payload (or that the wrapper returns/forwards the result),
referencing useConvexAction, mockActionRef, mockUseMutation and mutationFn to
locate the test.

In `@services/platform/app/hooks/__tests__/use-convex-mutation.test.ts`:
- Around line 56-60: The test currently asserts identity but useConvexMutation
wraps the original mutate (in useConvexMutation.ts) so update the test to assert
delegation: call the returned options.mutationFn with a sample arg and expect
mockMutationFn to have been called with that arg (use the same
mockMutationRef/mockMutationFn used in the test setup), or alternatively remove
the wrapper in useConvexMutation and set mutationFn: mutate directly so the
original identity assertion passes.

In `@services/platform/app/routes/dashboard/`$id/_knowledge/websites.tsx:
- Around line 33-38: The current check returning <WebsitesEmptyState
organizationId={organizationId} /> when paginatedResult.status === 'Exhausted'
&& paginatedResult.results.length === 0 hides filter controls; change the
condition to only render WebsitesEmptyState when there are no active
filters/search params. Detect active filters (e.g., status, search) from your
query/search params or the component's filter state and only return
WebsitesEmptyState if no filters are set; otherwise render the table (or an
empty-table placeholder) with filters still visible so users can clear them.
Update the conditional around WebsitesEmptyState (the paginatedResult.status /
paginatedResult.results.length check) to include a guard like "&&
!hasActiveFilters" where hasActiveFilters is derived from the relevant search
params or props.

In `@services/platform/app/routes/dashboard/`$id/custom-agents/$agentId.tsx:
- Around line 37-48: The loader currently prefetches getCustomAgentByVersion and
getCustomAgentVersions with only { customAgentId } which causes a cache miss
when a version is present in the URL because useCustomAgentByVersion(agentId,
versionNumber) includes the version in its query key; update the loader to
accept loaderDeps (extract the `v` search param into a versionNumber) and pass
that versionNumber into the convexQuery prefetch for
api.custom_agents.queries.getCustomAgentByVersion so the prefetched key matches
the component's key (keep using toId<'customAgents'>(params.agentId) for the
id).

In `@services/platform/app/routes/dashboard/`$id/settings/integrations.tsx:
- Around line 24-30: The loader currently prefetches only
integrations.queries.list; also prefetch the queries used by
useCurrentMemberContext and useSsoProvider so the component hydrates faster. In
the loader function add context.queryClient.prefetchQuery calls for the
underlying Convex queries those hooks use (i.e., call convexQuery with the same
api.* query identifiers that feed useCurrentMemberContext and useSsoProvider and
pass params.id or other needed args), mirroring the existing prefetch pattern
for api.integrations.queries.list so the hooks hit the cache on render.

In `@services/platform/app/routes/dashboard/`$id/settings/organization.tsx:
- Around line 15-21: The loader currently prefetches only the organization
query; add a second prefetch for the member context query so the component's
useCurrentMemberContext data is available. In the loader function, call void
context.queryClient.prefetchQuery(convexQuery(api.members.queries.getCurrentMemberContext,
<same-args-used-by-useCurrentMemberContext>)) alongside the existing
convexQuery(api.organizations.queries.getOrganization, { id: params.id }) call,
passing the same parameters the useCurrentMemberContext hook expects (e.g.,
params.id or no args) to mirror the route loader prefetch pattern.

In `@services/platform/convex/websites/list_websites_paginated.ts`:
- Around line 1-6: The docstring for the function in list_websites_paginated.ts
inaccurately claims remaining filters are applied via .filter(), but the
implementation only uses the compound index for status and never calls
.filter(); update the top comment to remove the reference to applying
`.filter()` and explicitly state that only the primary active filter (status) is
supported via the chosen 2-field compound index (or, if you prefer implementing
support for additional filters, add a `.filter()` pass after the `.paginate()`
call in the function `listWebsitesPaginated` to apply any remaining filters);
reference the function name `listWebsitesPaginated` (or the exported function in
this file) when making the change.

In `@services/platform/convex/websites/queries.ts`:
- Around line 26-35: listWebsitesPaginated is missing a returns validator while
listWebsites uses returns: v.array(websiteValidator); add a returns validator to
listWebsitesPaginated that validates the PaginationResult shape (e.g. returns:
paginationResultValidator(websiteValidator)) so runtime validation is
consistent, or if Convex cannot accept nested/custom validators here, add a
short inline comment on listWebsitesPaginated explaining why the returns clause
was omitted and reference PaginationResult/listWebsitesPaginated to help future
readers locate the rationale.

Comment on lines +56 to 60
it('uses the returned function as mutationFn', () => {
useConvexAction(mockActionRef);
const options = mockUseMutation.mock.calls[0]?.[0];
// @ts-expect-error -- calling mock onSettled directly for testing
await options.onSettled?.();

expect(callOrder).toEqual(['invalidate', 'invalidate', 'userOnSettled']);
expect(options).toHaveProperty('mutationFn', mockActionFn);
});

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

Test fails: mutationFn is a wrapper, not the raw mock.

The implementation wraps the action in an arrow function — mutationFn: (args) => action(args) — so it will never be referentially equal to mockActionFn. This is confirmed by the pipeline failure.

Assert behavioral equivalence instead of identity:

🐛 Proposed fix
   it('uses the returned function as mutationFn', () => {
     useConvexAction(mockActionRef);
     const options = mockUseMutation.mock.calls[0]?.[0];
-    expect(options).toHaveProperty('mutationFn', mockActionFn);
+    expect(options).toHaveProperty('mutationFn');
+    expect(typeof options.mutationFn).toBe('function');
+
+    const testArgs = { foo: 'bar' };
+    options.mutationFn(testArgs);
+    expect(mockActionFn).toHaveBeenCalledWith(testArgs);
   });
📝 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
it('uses the returned function as mutationFn', () => {
useConvexAction(mockActionRef);
const options = mockUseMutation.mock.calls[0]?.[0];
// @ts-expect-error -- calling mock onSettled directly for testing
await options.onSettled?.();
expect(callOrder).toEqual(['invalidate', 'invalidate', 'userOnSettled']);
expect(options).toHaveProperty('mutationFn', mockActionFn);
});
it('uses the returned function as mutationFn', () => {
useConvexAction(mockActionRef);
const options = mockUseMutation.mock.calls[0]?.[0];
expect(options).toHaveProperty('mutationFn');
expect(typeof options.mutationFn).toBe('function');
const testArgs = { foo: 'bar' };
options.mutationFn(testArgs);
expect(mockActionFn).toHaveBeenCalledWith(testArgs);
});
🧰 Tools
🪛 GitHub Actions: Test

[error] 59-59: AssertionError: expected { mutationFn: [Function mutationFn] } to have property "mutationFn" with value [Function Mock]

🪛 GitHub Check: Test

[failure] 59-59: app/hooks/tests/use-convex-action.test.ts > useConvexAction > uses the returned function as mutationFn
AssertionError: expected { mutationFn: [Function mutationFn] } to have property "mutationFn" with value [Function Mock]

  • Expected
  • Received
  • [Function Mock]
  • [Function mutationFn]

❯ app/hooks/tests/use-convex-action.test.ts:59:21

🤖 Prompt for AI Agents
In `@services/platform/app/hooks/__tests__/use-convex-action.test.ts` around lines
56 - 60, The test incorrectly asserts referential equality of the mutationFn
returned by useConvexAction to mockActionFn even though useConvexAction sets
mutationFn to a wrapper arrow function; update the assertion to verify
behavioral equivalence instead: grab options =
mockUseMutation.mock.calls[0]?.[0], ensure options.mutationFn is a function,
call options.mutationFn with a sample payload and then assert that mockActionFn
was called with that payload (or that the wrapper returns/forwards the result),
referencing useConvexAction, mockActionRef, mockUseMutation and mutationFn to
locate the test.

Comment on lines +56 to 60
it('uses the returned function as mutationFn', () => {
useConvexMutation(mockMutationRef);
const options = mockUseMutation.mock.calls[0]?.[0];
// @ts-expect-error -- calling mock onSettled directly for testing
await options.onSettled?.();

expect(callOrder).toEqual(['invalidate', 'invalidate', 'userOnSettled']);
expect(options).toHaveProperty('mutationFn', mockMutationFn);
});

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

Pipeline failure: mutationFn is a wrapper, not the mock itself.

The implementation at use-convex-mutation.ts:20 wraps the mutate function in an arrow: (args) => mutate(args), so it won't be referentially equal to mockMutationFn. The test should verify delegation behavior instead of identity.

🐛 Proposed fix — assert delegation instead of reference equality
   it('uses the returned function as mutationFn', () => {
     useConvexMutation(mockMutationRef);
     const options = mockUseMutation.mock.calls[0]?.[0];
-    expect(options).toHaveProperty('mutationFn', mockMutationFn);
+    expect(options).toHaveProperty('mutationFn');
+    const args = { id: '123' };
+    options.mutationFn(args);
+    expect(mockMutationFn).toHaveBeenCalledWith(args);
   });

Alternatively, if the arrow wrapper in use-convex-mutation.ts is not needed for type reasons, you could simplify the implementation to mutationFn: mutate (passing the function directly), which would make the original test assertion pass:

♻️ Alternative — simplify the implementation

In services/platform/app/hooks/use-convex-mutation.ts:

   return useMutation({
-    mutationFn: (args: FunctionArgs<Func>) => mutate(args),
+    mutationFn: mutate,
     ...options,
   });
📝 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
it('uses the returned function as mutationFn', () => {
useConvexMutation(mockMutationRef);
const options = mockUseMutation.mock.calls[0]?.[0];
// @ts-expect-error -- calling mock onSettled directly for testing
await options.onSettled?.();
expect(callOrder).toEqual(['invalidate', 'invalidate', 'userOnSettled']);
expect(options).toHaveProperty('mutationFn', mockMutationFn);
});
it('uses the returned function as mutationFn', () => {
useConvexMutation(mockMutationRef);
const options = mockUseMutation.mock.calls[0]?.[0];
expect(options).toHaveProperty('mutationFn');
const args = { id: '123' };
options.mutationFn(args);
expect(mockMutationFn).toHaveBeenCalledWith(args);
});
🧰 Tools
🪛 GitHub Actions: Test

[error] 59-59: AssertionError: expected { mutationFn: [Function mutationFn] } to have property "mutationFn" with value [Function Mock]

🪛 GitHub Check: Test

[failure] 59-59: app/hooks/tests/use-convex-mutation.test.ts > useConvexMutation > uses the returned function as mutationFn
AssertionError: expected { mutationFn: [Function mutationFn] } to have property "mutationFn" with value [Function Mock]

  • Expected
  • Received
  • [Function Mock]
  • [Function mutationFn]

❯ app/hooks/tests/use-convex-mutation.test.ts:59:21

🤖 Prompt for AI Agents
In `@services/platform/app/hooks/__tests__/use-convex-mutation.test.ts` around
lines 56 - 60, The test currently asserts identity but useConvexMutation wraps
the original mutate (in useConvexMutation.ts) so update the test to assert
delegation: call the returned options.mutationFn with a sample arg and expect
mockMutationFn to have been called with that arg (use the same
mockMutationRef/mockMutationFn used in the test setup), or alternatively remove
the wrapper in useConvexMutation and set mutationFn: mutate directly so the
original identity assertion passes.

Comment on lines +33 to 38
if (
paginatedResult.status === 'Exhausted' &&
paginatedResult.results.length === 0
) {
return <WebsitesEmptyState organizationId={organizationId} />;
}

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

Empty state renders even when a filter is active, hiding the filter UI.

When the user has a status filter applied (e.g., ?status=error) and no results match, paginatedResult.status === 'Exhausted' with results.length === 0 triggers WebsitesEmptyState. This replaces the entire table — including the filter controls — so the user can't clear the filter to see all websites. The empty state likely shows a "No websites yet" message with a create CTA, which is misleading when websites exist but don't match the filter.

Consider guarding against this when a search filter is active:

🐛 Proposed fix
   if (
     paginatedResult.status === 'Exhausted' &&
-    paginatedResult.results.length === 0
+    paginatedResult.results.length === 0 &&
+    !search.status
   ) {
     return <WebsitesEmptyState organizationId={organizationId} />;
   }
📝 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
if (
paginatedResult.status === 'Exhausted' &&
paginatedResult.results.length === 0
) {
return <WebsitesEmptyState organizationId={organizationId} />;
}
if (
paginatedResult.status === 'Exhausted' &&
paginatedResult.results.length === 0 &&
!search.status
) {
return <WebsitesEmptyState organizationId={organizationId} />;
}
🤖 Prompt for AI Agents
In `@services/platform/app/routes/dashboard/`$id/_knowledge/websites.tsx around
lines 33 - 38, The current check returning <WebsitesEmptyState
organizationId={organizationId} /> when paginatedResult.status === 'Exhausted'
&& paginatedResult.results.length === 0 hides filter controls; change the
condition to only render WebsitesEmptyState when there are no active
filters/search params. Detect active filters (e.g., status, search) from your
query/search params or the component's filter state and only return
WebsitesEmptyState if no filters are set; otherwise render the table (or an
empty-table placeholder) with filters still visible so users can clear them.
Update the conditional around WebsitesEmptyState (the paginatedResult.status /
paginatedResult.results.length check) to include a guard like "&&
!hasActiveFilters" where hasActiveFilters is derived from the relevant search
params or props.

Comment on lines +37 to +48
loader: ({ context, params }) => {
void context.queryClient.prefetchQuery(
convexQuery(api.custom_agents.queries.getCustomAgentByVersion, {
customAgentId: toId<'customAgents'>(params.agentId),
}),
);
void context.queryClient.prefetchQuery(
convexQuery(api.custom_agents.queries.getCustomAgentVersions, {
customAgentId: toId<'customAgents'>(params.agentId),
}),
);
},

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:

#!/bin/bash
# First, let's find the custom agents query definitions
fd -t f -e ts -e tsx custom_agents | head -20

Repository: tale-project/tale

Length of output: 114


🏁 Script executed:

#!/bin/bash
# Search for getCustomAgentByVersion query definition
rg "getCustomAgentByVersion" -t ts -t tsx -A 10 | head -50

Repository: tale-project/tale

Length of output: 88


🏁 Script executed:

#!/bin/bash
# Look at the specific file mentioned in the review
cat -n "services/platform/app/routes/dashboard/\$id/custom-agents/\$agentId.tsx" | head -100

Repository: tale-project/tale

Length of output: 4417


🏁 Script executed:

#!/bin/bash
# Find custom agents queries definition
fd -type f -name "*.ts" -path "*custom_agents*" | grep -E "(queries|hook)" | head -20

Repository: tale-project/tale

Length of output: 231


🏁 Script executed:

#!/bin/bash
# Search for getCustomAgentByVersion in API/query definitions
rg "getCustomAgentByVersion" -A 15 -B 2

Repository: tale-project/tale

Length of output: 4419


🏁 Script executed:

#!/bin/bash
# Find useCustomAgentByVersion hook
rg "useCustomAgentByVersion" -t ts -A 15 -B 2

Repository: tale-project/tale

Length of output: 5079


🏁 Script executed:

#!/bin/bash
# Check other loaders in the codebase to see if they use search parameter
rg "loader:.*\(\s*\{.*search" -A 5 | head -40

Repository: tale-project/tale

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Look for TanStack Router loader patterns with search
rg "loader:\s*\(\s*\{" -A 3 | grep -E "(loader:|search)" | head -30

Repository: tale-project/tale

Length of output: 1057


🌐 Web query:

TanStack Router loader function parameters search

💡 Result:

In TanStack Router, a route loader is called with one argument: an object that includes (among others) params, deps, context, and location. (tanstack.com)

Loader parameter shape (relevant parts)

  • params: path params (e.g. $postId) (tanstack.com)
  • deps: whatever your loaderDeps returns (defaults to {}) (tanstack.com)
  • location: the current location (includes search info) (tanstack.com)

How to use search params in a loader (recommended)

Use loaderDeps to pick the specific search params your loader depends on, then read them from deps inside loader:

export const Route = createFileRoute('/posts')({
  loaderDeps: ({ search: { offset, limit } }) => ({ offset, limit }),
  loader: ({ deps: { offset, limit } }) => fetchPosts({ offset, limit }),
})

(tanstack.com)

This is preferred because changes in those deps will correctly trigger reloads without invalidating cache for unrelated search params. (tanstack.com)

Citations:


Pass versionNumber to prefetch query using loaderDeps — cache miss when ?v=X is present.

The component queries useCustomAgentByVersion(agentId, versionNumber) with the v search param, but the loader only passes { customAgentId }. When a version is specified in the URL, the prefetched query key won't match the component's query key, causing a cache miss.

TanStack Router loaders don't receive search directly. Use loaderDeps to extract and pass the search param:

+  loaderDeps: ({ search: { v } }) => ({ versionNumber: v }),
-  loader: ({ context, params }) => {
+  loader: ({ context, params, deps: { versionNumber } }) => {
     void context.queryClient.prefetchQuery(
       convexQuery(api.custom_agents.queries.getCustomAgentByVersion, {
         customAgentId: toId<'customAgents'>(params.agentId),
+        versionNumber,
       }),
     );
📝 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
loader: ({ context, params }) => {
void context.queryClient.prefetchQuery(
convexQuery(api.custom_agents.queries.getCustomAgentByVersion, {
customAgentId: toId<'customAgents'>(params.agentId),
}),
);
void context.queryClient.prefetchQuery(
convexQuery(api.custom_agents.queries.getCustomAgentVersions, {
customAgentId: toId<'customAgents'>(params.agentId),
}),
);
},
loaderDeps: ({ search: { v } }) => ({ versionNumber: v }),
loader: ({ context, params, deps: { versionNumber } }) => {
void context.queryClient.prefetchQuery(
convexQuery(api.custom_agents.queries.getCustomAgentByVersion, {
customAgentId: toId<'customAgents'>(params.agentId),
versionNumber,
}),
);
void context.queryClient.prefetchQuery(
convexQuery(api.custom_agents.queries.getCustomAgentVersions, {
customAgentId: toId<'customAgents'>(params.agentId),
}),
);
},
🤖 Prompt for AI Agents
In `@services/platform/app/routes/dashboard/`$id/custom-agents/$agentId.tsx around
lines 37 - 48, The loader currently prefetches getCustomAgentByVersion and
getCustomAgentVersions with only { customAgentId } which causes a cache miss
when a version is present in the URL because useCustomAgentByVersion(agentId,
versionNumber) includes the version in its query key; update the loader to
accept loaderDeps (extract the `v` search param into a versionNumber) and pass
that versionNumber into the convexQuery prefetch for
api.custom_agents.queries.getCustomAgentByVersion so the prefetched key matches
the component's key (keep using toId<'customAgents'>(params.agentId) for the
id).

Comment on lines +24 to +30
loader: ({ context, params }) => {
void context.queryClient.prefetchQuery(
convexQuery(api.integrations.queries.list, {
organizationId: params.id,
}),
);
},

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.

🧹 Nitpick | 🔵 Trivial

Consider prefetching other queries used by this page.

The loader prefetches integrations.queries.list, but the component also depends on useCurrentMemberContext and useSsoProvider (lines 79-83). If those queries are similarly expensive, prefetching them here would further reduce time-to-content. Otherwise, this is fine as-is.

🤖 Prompt for AI Agents
In `@services/platform/app/routes/dashboard/`$id/settings/integrations.tsx around
lines 24 - 30, The loader currently prefetches only integrations.queries.list;
also prefetch the queries used by useCurrentMemberContext and useSsoProvider so
the component hydrates faster. In the loader function add
context.queryClient.prefetchQuery calls for the underlying Convex queries those
hooks use (i.e., call convexQuery with the same api.* query identifiers that
feed useCurrentMemberContext and useSsoProvider and pass params.id or other
needed args), mirroring the existing prefetch pattern for
api.integrations.queries.list so the hooks hit the cache on render.

Comment on lines +15 to +21
loader: ({ context, params }) => {
void context.queryClient.prefetchQuery(
convexQuery(api.organizations.queries.getOrganization, {
id: params.id,
}),
);
},

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.

🧹 Nitpick | 🔵 Trivial

Consider also prefetching the current member context query.

The component depends on both useOrganization and useCurrentMemberContext for its loading state (Line 72), but the loader only prefetches the organization query. Adding a prefetch for the member context query would be consistent with the tone-of-voice route loader pattern, which prefetches all queries consumed by its component.

🤖 Prompt for AI Agents
In `@services/platform/app/routes/dashboard/`$id/settings/organization.tsx around
lines 15 - 21, The loader currently prefetches only the organization query; add
a second prefetch for the member context query so the component's
useCurrentMemberContext data is available. In the loader function, call void
context.queryClient.prefetchQuery(convexQuery(api.members.queries.getCurrentMemberContext,
<same-args-used-by-useCurrentMemberContext>)) alongside the existing
convexQuery(api.organizations.queries.getOrganization, { id: params.id }) call,
passing the same parameters the useCurrentMemberContext hook expects (e.g.,
params.id or no args) to mirror the route loader prefetch pattern.

Comment on lines +1 to +6
/**
* List websites using Convex native .paginate() for use with usePaginatedQuery.
*
* Dispatches to the best 2-field compound index based on the primary active
* filter, then applies .filter() for any remaining filters.
*/

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 | 🟡 Minor

Docstring mentions .filter() for remaining filters, but the implementation never calls .filter().

Line 5 states the function "applies .filter() for any remaining filters," but no code path invokes .filter(). Currently only status is supported and it's handled via the compound index. Update the docstring to reflect the actual behavior, or implement the remaining-filter path if it's intended.

📝 Suggested docstring fix
 /**
  * List websites using Convex native .paginate() for use with usePaginatedQuery.
  *
  * Dispatches to the best 2-field compound index based on the primary active
- * filter, then applies .filter() for any remaining filters.
+ * filter; falls back to the by_organizationId index when no filter is active.
  */
🤖 Prompt for AI Agents
In `@services/platform/convex/websites/list_websites_paginated.ts` around lines 1
- 6, The docstring for the function in list_websites_paginated.ts inaccurately
claims remaining filters are applied via .filter(), but the implementation only
uses the compound index for status and never calls .filter(); update the top
comment to remove the reference to applying `.filter()` and explicitly state
that only the primary active filter (status) is supported via the chosen 2-field
compound index (or, if you prefer implementing support for additional filters,
add a `.filter()` pass after the `.paginate()` call in the function
`listWebsitesPaginated` to apply any remaining filters); reference the function
name `listWebsitesPaginated` (or the exported function in this file) when making
the change.

Comment on lines +26 to +35
export const listWebsitesPaginated = queryWithRLS({
args: {
paginationOpts: paginationOptsValidator,
organizationId: v.string(),
status: v.optional(v.string()),
},
handler: async (ctx, args) => {
return await listWebsitesPaginatedHelper(ctx, args);
},
});

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.

🧹 Nitpick | 🔵 Trivial

Consider adding a returns validator for consistency.

listWebsites (Line 12) declares returns: v.array(websiteValidator), but listWebsitesPaginated omits a returns clause. If Convex supports return validators on paginated queries, adding one would improve runtime validation consistency. If not feasible due to the PaginationResult wrapper shape, a brief comment explaining the omission would help future readers.

🤖 Prompt for AI Agents
In `@services/platform/convex/websites/queries.ts` around lines 26 - 35,
listWebsitesPaginated is missing a returns validator while listWebsites uses
returns: v.array(websiteValidator); add a returns validator to
listWebsitesPaginated that validates the PaginationResult shape (e.g. returns:
paginationResultValidator(websiteValidator)) so runtime validation is
consistent, or if Convex cannot accept nested/custom validators here, add a
short inline comment on listWebsitesPaginated explaining why the returns clause
was omitted and reference PaginationResult/listWebsitesPaginated to help future
readers locate the rationale.

@yannickmonney yannickmonney merged commit 15fd0bd into main Feb 14, 2026
16 checks passed
@yannickmonney yannickmonney deleted the feat/simplify-tanstack-query branch February 14, 2026 15:07
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.

1 participant