#paginated query result
22 messages · Page 1 of 1 (latest)
You can only access what your Convex query actually returns. For a paginated query, that must be (or at least include) the PaginationResult shape that .paginate returns: { page, isDone, continueCursor, ... }.[PaginationResult; Query.paginate]
On the client, usePaginatedQuery does not expose that raw object. Instead it:
- Calls your paginated query repeatedly,
- Concatenates all
pagearrays into a singleresultsarray, - Manages cursors internally,
- Returns only:
{
results: Item[];
status: "LoadingFirstPage" | "CanLoadMore" | "LoadingMore" | "Exhausted";
isLoading: boolean;
loadMore: (n: number) => void;
}
```[[usePaginatedQuery](<https://docs.convex.dev/api/modules/react#usepaginatedquery>); [UsePaginatedQueryResult](<https://docs.convex.dev/api/modules/react#usepaginatedqueryresult>)]
So from `organizationsPagination` you can only read:
```ts
organizationsPagination.results
organizationsPagination.status
organizationsPagination.isLoading
organizationsPagination.loadMore
If getOrgListForSwitcher returns extra fields alongside the PaginationResult (e.g. { ...paginationResult, totalCount }), those extra fields are not surfaced by usePaginatedQuery according to the docs; only the concatenated results are.[Paginating within React]
If you need additional values (like counts, metadata, etc.):
- Return them as part of each item in
page(so they appear insideresults), or - Expose a separate non‑paginated query for that metadata and call it with
useQuery.
The knowledge sources don’t document any way to get the raw PaginationResult or arbitrary extra return fields out of usePaginatedQuery.
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@soft grove Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@covert shore static async listByOrgName(
ctx: QueryCtx,
workosUserId: string,
paginationOpts: PaginationOptions,
search?: string,
excludeWorkosOrganizationId?: string,
) {
const organizationMemberships = stream(ctx.db, schema)
.query("organizationMemberships")
.withIndex("by_workosUserId", (q) => q.eq("workosUserId", workosUserId))
.order("desc");
const organizations = organizationMemberships
.map(async (membership) => {
return await Organization.getByWorkOSId(
ctx,
membership.workosOrganizationId,
);
})
.filterWith(async (organization) =>
search
? organization.name.toLowerCase().includes(search.toLowerCase())
: true,
);
const MAX_ITEMS_PER_PAGE = 5;
return organizations.paginate({
...paginationOpts,
numItems: Math.min(paginationOpts.numItems, MAX_ITEMS_PER_PAGE),
});
}
how can i exclude the organization with the given id
Let me check the knowledge base for you again. One moment! :books:
You can add another condition to your filterWith predicate to skip the organization whose id you want to exclude.
Since you’re already using filterWith on the stream, just combine the search check with an id check before returning true:
static async listByOrgName(
ctx: QueryCtx,
workosUserId: string,
paginationOpts: PaginationOptions,
search?: string,
excludeWorkosOrganizationId?: string,
) {
const organizationMemberships = stream(ctx.db, schema)
.query("organizationMemberships")
.withIndex("by_workosUserId", (q) => q.eq("workosUserId", workosUserId))
.order("desc");
const organizations = organizationMemberships
.map(async (membership) => {
return await Organization.getByWorkOSId(
ctx,
membership.workosOrganizationId,
);
})
.filterWith(async (organization) => {
if (!organization) return false;
// Exclude a specific organization id if provided
if (
excludeWorkosOrganizationId &&
organization.workosOrganizationId === excludeWorkosOrganizationId
) {
return false;
}
// Existing search filter
if (search) {
return organization.name
.toLowerCase()
.includes(search.toLowerCase());
}
return true;
});
const MAX_ITEMS_PER_PAGE = 5;
return organizations.paginate({
...paginationOpts,
numItems: Math.min(paginationOpts.numItems, MAX_ITEMS_PER_PAGE),
});
}
filterWith is designed exactly for this kind of arbitrary async TypeScript predicate on a stream before pagination, so adding the extra id check there is the right place to do it.[Merging streams]
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@soft grove Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@covert shore static async listByOrgName(
ctx: QueryCtx,
workosUserId: string,
paginationOpts: PaginationOptions,
search?: string,
currentOrganizationId?: string,
) {
const organizationMemberships = stream(ctx.db, schema)
.query("organizationMemberships")
.withIndex("by_workosUserId", (q) => q.eq("workosUserId", workosUserId))
.order("desc");
const organizations = organizationMemberships
.map(async (membership) => {
return await Organization.getByWorkOSId(
ctx,
membership.workosOrganizationId,
);
})
.filterWith(async (organization) => {
// If a search term is provided, filter by organization name
if (search) {
return organization.name.toLowerCase().includes(search.toLowerCase());
}
return true;
});
const MAX_ITEMS_PER_PAGE = 5;
return organizations.paginate({
...paginationOpts,
numItems: Math.min(paginationOpts.numItems, MAX_ITEMS_PER_PAGE),
});
}
how can i set the current organization always at first position, except when the search term is provided
Let me check the knowledge base for you again. One moment! :books:
You can do this by:
- Letting the stream +
paginategive you a normal page. - Reordering the
pagearray before returning it: move the current org to the front when no search term is provided.
paginate returns an object like { page, isDone, continueCursor, ... }, and you’re allowed to transform page before returning it from your query function.[Paginated queries]
Applied to your code:
static async listByOrgName(
ctx: QueryCtx,
workosUserId: string,
paginationOpts: PaginationOptions,
search?: string,
currentOrganizationId?: string,
) {
const organizationMemberships = stream(ctx.db, schema)
.query("organizationMemberships")
.withIndex("by_workosUserId", (q) => q.eq("workosUserId", workosUserId))
.order("desc");
const organizations = organizationMemberships
.map(async (membership) => {
return await Organization.getByWorkOSId(
ctx,
membership.workosOrganizationId,
);
})
.filterWith(async (organization) => {
if (search) {
return organization.name
.toLowerCase()
.includes(search.toLowerCase());
}
return true;
});
const MAX_ITEMS_PER_PAGE = 5;
const result = await organizations.paginate({
...paginationOpts,
numItems: Math.min(paginationOpts.numItems, MAX_ITEMS_PER_PAGE),
});
// When searching, keep the natural order
if (search || !currentOrganizationId) {
return result;
}
// Otherwise, move current org to the front of this page
const page = [...result.page];
const idx = page.findIndex(
(org) => org.workosOrganizationId === currentOrganizationId,
);
if (idx > 0) {
const [current] = page.splice(idx, 1);
page.unshift(current);
}
return { ...result, page };
}
This keeps pagination behavior intact and only changes the order within each returned page, which is explicitly supported.[Paginated queries]
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@soft grove Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@covert shore static async listByOrgName(
ctx: QueryCtx,
workosUserId: string,
paginationOpts: PaginationOptions,
search?: string,
currentOrganizationId?: string,
) {
const organizationMemberships = stream(ctx.db, schema)
.query("organizationMemberships")
.withIndex("by_workosUserId", (q) => q.eq("workosUserId", workosUserId))
.order("desc");
const organizations = organizationMemberships
.map(async (membership) => {
return await Organization.getByWorkOSId(
ctx,
membership.workosOrganizationId,
);
})
.filterWith(async (organization) => {
// If a search term is provided, filter by organization name
if (search) {
return organization.name.toLowerCase().includes(search.toLowerCase());
}
return true;
});
const result = await organizations.paginate({
...paginationOpts,
});
// If there's a current organization, move it to the front of the list
if (currentOrganizationId) {
const page = [...result.page];
const idx = page.findIndex(
(org) => org.workosId === currentOrganizationId,
);
if (idx > 0) {
const [current] = page.splice(idx, 1);
page.unshift(current);
}
return { ...result, page };
}
return result;
}
when there are 5 results and i display 5 in the frontend then the state is still that it can load more. when i load more then it goes to exhaustet but the results didnt change of course. how can i fix this
Let me check the knowledge base for you again. One moment! :books:
You’re running into a known caveat of using stream(...).paginate(...): unlike ctx.db.query().paginate, streaming pagination does not automatically maintain the “fully reactive, contiguous pages” guarantees, and you have to be more careful with cursors and page boundaries yourself.[Merging streams]
In particular, the docs call out:
If you’re using reactive pagination, like the React
usePaginatedQueryhook, holes or overlaps may develop between pages. To avoid such problems, you can pass thecontinueCursorback in asendCursorto pagination opts, which will ensure that a page ends at the same place where the next page starts. This requires calling each query twice, since you don't know the continueCursor until after the first execution.[Merging streams]
In your case:
- You’re using
stream(...).paginate(...)and then reorderingresult.pageto move the current org to the front. usePaginatedQueryon the client only sees the concatenatedpages and theisDone/cursor state coming from the server; it doesn’t know you’ve reordered or that the last page is “full but actually exhausted”.- Because streaming pagination doesn’t have the same automatic guarantees, the
isDoneflag / cursor can say “more pages possible” even when, after your transformation, the client-visible list is effectively complete.
From the docs, there isn’t a server‑side API to “fix up” isDone or the cursor after you transform the page, and there’s no documented way to override how usePaginatedQuery interprets that. The recommended way to avoid these pagination inconsistencies with streams is:
- Avoid using reactive pagination (
usePaginatedQuery) withstream(...).paginateif you need strict correctness; or - Use non‑reactive pagination (e.g. call the query from an action or manually manage cursors) so you’re not relying on Convex’s reactive pagination guarantees; or
- If you stick with streams +
usePaginatedQuery, follow the guidance to passcontinueCursorback asendCursorand accept that you may need extra logic and multiple calls per page to keep things consistent.[Merging streams]
The knowledge sources don’t describe a supported way to:
- Adjust
isDoneyourself after transformingpage, or - Make
usePaginatedQuerystop offeringloadMorewhen you know (from your own logic) that there are no more items.
So based on the docs, the robust fix is to either:
- Re‑implement this query using
ctx.db.query().paginate(nostream) so you get the built‑in reactive pagination guarantees, then do yourpagereordering as you already do; or - Treat this as non‑reactive pagination and manage cursors and “can load more” state yourself instead of relying on
usePaginatedQuery.
There isn’t enough information in the sources to give a more precise, code‑level fix that keeps stream(...).paginate + usePaginatedQuery and also guarantees the status transitions you want.
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@soft grove Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@covert shore how can i test a convex workflow from the workflow component with convex-test?
Let me check the knowledge base for you again. One moment! :books:
You can test workflows from the @convex-dev/workflow component with convex-test the same way you test other components: initialize convexTest, register the component, then call the functions that start or interact with the workflow.
The docs show the general pattern for testing components with convex-test:
- Initialize
convexTestand register the component
Most components expose a /test entrypoint with a helper that calls t.registerComponent(...) for you.[Using components; Authoring components]
For a component that exports such a helper (example uses @convex-dev/agent, but the pattern is the same):
// convex/workflow.test.ts
import { expect, test } from "vitest";
import { convexTest } from "convex-test";
import workflowTest from "@convex-dev/workflow/test"; // hypothetical test helper
import { components } from "./_generated/api";
// Define once in a shared helper if you like
export function initConvexTest() {
const t = convexTest();
workflowTest.register(t); // registers the workflow component
return t;
}
test("my workflow behavior", async () => {
const t = initConvexTest();
// Now you can call functions that use `components.workflow`
// e.g. a mutation that starts your workflow
const id = await t.mutation(components.myApp.kickoffWorkflow, { /* args */ });
// Or call component API directly if it exposes any
// await t.query(components.workflow.someQuery, {...});
// Assert on DB state, etc.
const something = await t.run(async (ctx) => {
return await ctx.db.query("someTable").collect();
});
expect(something).toMatchObject(/* ... */);
});
If the workflow component does not yet export a /test helper, you can register it manually by passing its schema and modules to t.registerComponent, as shown in the docs:[Using components; Authoring components]
/// <reference types="vite/client" />
import { test } from "vitest";
import { convexTest } from "convex-test";
import workflowSchema from "path/to/workflow/component/schema";
const workflowModules = import.meta.glob("path/to/workflow/component/**/*.ts");
test("workflow component manually registered", async () => {
const t = convexTest();
t.registerComponent("workflow", workflowSchema, workflowModules);
await t.run(async (ctx) => {
// use components.workflow.* from your app code
});
});
- What you can assert
convex-test is a mock backend, so you can:
- Call mutations/actions that start workflows (
workflow.start(...)in your code).[Workflow usage] - Use
t.runto inspect DB state or any tables the workflow writes to.[Using convex-test] - Assert that follow‑up mutations/actions ran by checking their side effects.