#pagination

46 messages · Page 1 of 1 (latest)

tired oasis
#

can you give an example for paginating pages? prev and next

crude stirrupBOT
#

Hi @tired oasis. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:

#

Certainly! In Convex, the standard approach for pagination is cursor-based, typically using the .paginate() method in your query function and the usePaginatedQuery hook in your React component. This pattern is designed for "infinite scroll" or "Load More" UIs, where you can load the next page of results. However, built-in pagination is one-directional (forward only)—there is no built-in "load previous" (prev) page functionality.

Example: Basic Next Page Pagination

Query function:

import { query } from "./_generated/server";
import { paginationOptsValidator } from "convex/server";

export const list = query({
  args: { paginationOpts: paginationOptsValidator },
  handler: async (ctx, args) => {
    return await ctx.db
      .query("messages")
      .order("desc")
      .paginate(args.paginationOpts);
  },
});

Source: Paginated Queries

React component:

import { usePaginatedQuery } from "convex/react";
import { api } from "../convex/_generated/api";

export function App() {
  const { results, status, loadMore } = usePaginatedQuery(
    api.messages.list,
    {},
    { initialNumItems: 5 }
  );
  return (
    <div>
      {results?.map(({ _id, body }) => <div key={_id}>{body}</div>)}
      <button onClick={() => loadMore(5)} disabled={status !== "CanLoadMore"}>
        Load More
      </button>
    </div>
  );
}

Source: Paginating within React Components

What about "Prev" (Previous Page) Pagination?

Convex's built-in pagination does not support loading previous pages out-of-the-box. If you need true bi-directional pagination (jumping to a specific page and scrolling both up and down), you have a couple of options:

#

Summary:

  • For "next" page, use .paginate() and usePaginatedQuery as shown above.
  • For "prev" page, consider advanced patterns with getPage or community solutions, as built-in support is currently one-directional.

If you need a concrete example of a custom prev/next implementation using getPage, let me know!

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@tired oasis Kapa.ai is still learning and improving, please let me know how I did by reacting below.

tired oasis
#

@crude stirrup so If I want offeset pagination, I need to use the aggregate component?

crude stirrupBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Yes, if you want true offset-based pagination in Convex (e.g., "jump to page 10" or "show items 100–110"), you should use the Aggregate component. The built-in .paginate() and usePaginatedQuery are cursor-based and do not support skipping a specific number of items efficiently, especially for large datasets. The Aggregate component is specifically designed to enable efficient offset-based pagination, allowing you to jump directly to any page without loading all previous items.

For example, you can use the Aggregate component to map an offset to an index key and then fetch the desired page:

export const pageOfPhotos = query({
  args: { offset: v.number(), numItems: v.number(), album: v.string() },
  handler: async (ctx, { offset, numItems, album }) => {
    const { key } = await photos.at(ctx, offset, { namespace: album });
    return await ctx.db.query("photos")
      .withIndex("by_album_creation_time", q => q.eq("album", album).gte("_creationTime", key))
      .take(numItems);
  },
});

This approach is recommended for large datasets where you need to support jumping to arbitrary pages efficiently Components > Aggregate > Example use-cases Stack: Efficient COUNT, SUM, MAX with the Aggregate Component.

So, for offset pagination in Convex, the Aggregate component is the right tool.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@tired oasis Kapa.ai is still learning and improving, please let me know how I did by reacting below.

tired oasis
#

@crude stirrup does it make sense to add an aggregate for all of my tables? say users, posts, messages, so I can get the full count?

crude stirrupBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

It only makes sense to add an Aggregate component to a table if you need efficient aggregate operations—such as count, sum, min, max, or offset-based pagination—on that table. For example, if you frequently need to know the total number of users, posts, or messages, or need to support features like leaderboards or paginated views by offset, then adding an aggregate is beneficial.

However, if you rarely or never need to perform these aggregate operations on a table, adding an aggregate may be unnecessary overhead. The Aggregate component is most valuable when you need real-time, efficient, and scalable aggregate queries, especially as your data grows. Convex does not provide built-in aggregate queries like SELECT COUNT(*), so the Aggregate component is the recommended way to achieve this functionality in a performant way Components > Aggregate Efficient COUNT, SUM, MAX with the Aggregate Component.

In summary:

  • Yes, it makes sense to add an aggregate for tables where you need efficient counts or other aggregate operations.
  • No, it is not necessary for every table—only add it where you need those features.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@tired oasis Kapa.ai is still learning and improving, please let me know how I did by reacting below.

tired oasis
#

@crude stirrup say I have an aggregate table for my posts, when I add or delete a post, do I need to write a mutation for my aggregate?

crude stirrupBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Yes, when you have an aggregate for your posts table, you need to ensure that every time you add, delete, or update a post, you also update the aggregate in the same mutation. This is necessary to keep the aggregate in sync with your source-of-truth table. If you forget to update the aggregate, your counts or other aggregate values can become incorrect.

There are three main ways to keep your aggregate in sync:

  1. Manually update the aggregate in every mutation that modifies the table (e.g., call aggregate.insert, aggregate.delete, or aggregate.replace as appropriate).
  2. Encapsulate all writes in helper functions that always update both the table and the aggregate.
  3. Use triggers (recommended), which automatically update the aggregate whenever the table changes, so you don't have to remember to do it manually in every mutation. This reduces the risk of your aggregate getting out of sync Components > Aggregate > Operations Efficient COUNT, SUM, MAX with the Aggregate Component.

If you use triggers, you do not need to manually write aggregate updates in every mutation—the trigger will handle it for you. Otherwise, you must remember to update the aggregate yourself each time you modify the table.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@tired oasis Kapa.ai is still learning and improving, please let me know how I did by reacting below.

tired oasis
#

@crude stirrup the trigger can fail right? I'd rather have the mutation for my agregate where my table update is also happening

crude stirrupBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Your thinking is correct, and it aligns with best practices discussed in the Convex documentation.

Triggers in Convex are designed to run atomically within the same mutation that changes the data, so if a trigger fails, the entire mutation (including the table update) will also fail and be rolled back. This means you won't end up with your table and aggregate out of sync due to a trigger failure—either both succeed, or both fail. However, using triggers can introduce "spooky action at a distance," making it harder to reason about your code, since side effects are not always explicit.

The Convex docs recommend, for clarity and maintainability, to encapsulate all writes to a table in explicit helper functions that also update the aggregate. This way, you always know when and how your aggregate is being updated, and you avoid the hidden side effects of triggers. Here’s an example of the recommended pattern:

// All inserts to the "posts" table go through this function.
async function insertPost(ctx, post) {
  const id = await ctx.db.insert("posts", post);
  const doc = await ctx.db.get(id);
  await aggregate.insert(ctx, doc!);
}

Then, in your mutation:

export const createPost = mutation(async (ctx, post) => {
  await insertPost(ctx, post);
});

This approach keeps your logic explicit and easier to follow, and you have full control over error handling and transactionality Components > Aggregate > Operations Triggers Best Practices.

#

Summary:

  • Triggers are atomic with mutations, but can make code harder to reason about.
  • Explicitly updating your aggregate in the same mutation as your table update is a clear and recommended approach.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@tired oasis Kapa.ai is still learning and improving, please let me know how I did by reacting below.

tired oasis
#

@crude stirrup in a single query, how much data can I pull

crude stirrupBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

In a single query, you can pull up to 16 MiB of data and scan up to 32,000 documents. If you use .collect(), .take(n), or similar methods, the total data returned and scanned must not exceed these limits. Note that data not returned due to a filter still counts as scanned. If you exceed these limits, the query will error out Production > State > Limits.

For full text search queries, the maximum number of results you can retrieve in a single query is 1,024 documents Search > Full Text Search > Limits.

If you need to retrieve more data, you should use pagination to split your query into multiple requests Queries that scale.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@tired oasis Kapa.ai is still learning and improving, please let me know how I did by reacting below.

tired oasis
#

@crude stirrup for the pagination that convex already supports, can I use tanstack query with it as well? or should I just use it as stand alone

crude stirrupBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Yes, you can use Convex’s built-in pagination with TanStack Query, but there are some important details:

  • There is no TanStack-specific version of usePaginatedQuery yet. You can use the regular Convex usePaginatedQuery hook alongside TanStack Query in your app, and they will work together since they share the same Convex client instance. This means you can use TanStack Query for some queries and Convex’s React hooks for paginated queries (Discord support thread: usePaginatedQuery with tanstack react query, TanStack Start docs).

  • If you want to use TanStack Query’s own infinite query patterns (like useInfiniteQuery), you’ll need to implement your own paginated query logic on the backend and handle cursors manually. This approach works, but you will lose Convex’s built-in reactivity for paginated queries, as TanStack Query’s infinite query is not fully integrated with Convex’s reactive pagination (Discord support thread: convex-react-query state and plan on useInfiniteQuery hook support).

Summary:

  • For best reactivity and simplicity, use Convex’s usePaginatedQuery as a stand-alone hook, even in a TanStack Query app.
  • If you need TanStack Query’s caching and pagination patterns, you can implement your own paginated queries, but you will lose some of Convex’s reactivity.
#

@tired oasis Kapa.ai is still learning and improving, please let me know how I did by reacting below.

tired oasis
#

@crude stirrup how can I use usePaginatedQuery with tanstack?

crude stirrupBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

You can use Convex’s usePaginatedQuery hook alongside TanStack Query in your app, but there isn’t a TanStack-specific version of usePaginatedQuery yet. The recommended approach is to use the regular Convex usePaginatedQuery hook for paginated queries, even if you are using TanStack Query elsewhere in your application. Both sets of hooks share the same Convex client, so you can mix and match them as needed.

Here’s a direct answer from Convex support:

"You can use the regular usePaginatedQuery with TanStack, there isn’t a TanStack specific version yet"
Discord support thread: usePaginatedQuery with tanstack react query

So, simply use usePaginatedQuery as you normally would in your React components. For example:

import { usePaginatedQuery } from "convex/react";
import { api } from "../convex/_generated/api";

export function App() {
  const { results, status, loadMore } = usePaginatedQuery(
    api.messages.list,
    {},
    { initialNumItems: 5 }
  );
  // ...render your paginated list
}

This will work seamlessly, even in a project that also uses TanStack Query for other data fetching needs.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@tired oasis Kapa.ai is still learning and improving, please let me know how I did by reacting below.

tired oasis
#

@crude stirrup how to handle exceptions thrown from usePaginatedQuery

crude stirrupBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

To handle exceptions thrown from usePaginatedQuery, the recommended approach is to use a React error boundary. When an error is thrown in your paginated query function, it will propagate to the client and be thrown from your usePaginatedQuery call site. Wrapping your component (or part of your app) in an error boundary allows you to catch these errors, render fallback UI, and optionally report the error to an exception handling service.

Here’s a summary of the approach:

  • Wrap your component with an error boundary:

    <ErrorBoundary>
      <YourPaginatedComponent />
    </ErrorBoundary>
    

    This will catch any errors thrown by usePaginatedQuery and display your fallback UI instead of crashing the app.

  • Why not try/catch?
    Since usePaginatedQuery is a React hook, you cannot use try/catch directly around it in your component body. Error boundaries are the React way to handle these errors.

  • Custom error handling:
    If you want more granular control, you can create your own error boundary component to display custom messages or handle specific error types.

Reference:

"If your query function hits an error, the error will be sent to the client and thrown from your useQuery call site. The best way to handle these errors is with a React error boundary component."
Functions > Error Handling

This pattern applies to both useQuery and usePaginatedQuery in Convex.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@tired oasis Kapa.ai is still learning and improving, please let me know how I did by reacting below.

fickle panther
#

@crude stirrup I have an action that I schedule, how do I propagate user auth to this action ?

crude stirrupBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

When you schedule an action in Convex (for example, using ctx.scheduler.runAfter), the auth context is not automatically propagated to the scheduled function. This means that when your scheduled action runs, ctx.auth.getUserIdentity() will return null, even if the original caller was authenticated. This is by design, since the scheduled function may run at a later time when the user may no longer be authenticated.

To propagate user authentication to a scheduled action, you should explicitly pass the user information (such as the user ID or the full user identity) as an argument to the scheduled function. Then, in your scheduled action, use this argument to perform any necessary authorization or user-specific logic.

Here’s the official guidance:

The auth is not propagated from the scheduling to the scheduled function. If you want to authenticate or check authorization, you'll have to pass the requisite user information in as a parameter.
Scheduling > Scheduled Functions > Auth

This approach is also confirmed in community support:

just use internal* and pass the UserIdentity or whatever directly
Discord support thread: Is there a way to pass the auth context in workpool queues?

Summary:

  • Auth context is not automatically passed to scheduled actions.
  • Pass user info (e.g., user ID) as an argument when scheduling the action.
  • Use that argument in your action for any user-specific logic or authorization.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@fickle panther Kapa.ai is still learning and improving, please let me know how I did by reacting below.

fickle panther
#

@crude stirrup Could you provide a code example for passing the full user identity to a function that I will schedule.

crude stirrupBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Certainly! To pass the full user identity to a scheduled function in Convex, you should:

  1. Retrieve the user identity in your initial function.
  2. Pass it as an argument when scheduling the action.
  3. Accept and use it as an argument in your scheduled function.

Here’s a code example based on the guidance from the knowledge sources:

// In your mutation or query where you schedule the action:
export const scheduleAction = mutation({
  args: { /* your args */ },
  handler: async (ctx, args) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) throw new Error("Unauthenticated");

    // Schedule the action, passing the full identity as an argument
    await ctx.scheduler.runAfter(
      0,
      internal.actions.scheduledAction,
      { ...args, userIdentity: identity }
    );
  },
});
// In your scheduled action:
export const scheduledAction = internalAction({
  args: {
    /* your args */,
    userIdentity: v.any(), // Accept the user identity as an argument
  },
  handler: async (ctx, { userIdentity, ...otherArgs }) => {
    // Use userIdentity as needed
    if (!userIdentity) throw new Error("No user identity provided");
    // ...your logic here
  },
});

This approach is recommended because the auth context is not automatically propagated to scheduled functions; you must pass the user information explicitly as an argument Scheduling > Scheduled Functions > Auth Discord support thread: Is there a way to pass the auth context in workpool queues?.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: