#Agents

25 messages · Page 1 of 1 (latest)

distant rivet
#

When using @convex-dev/agent package, can we:

  1. Preload the chat messages on SSR (via preloadQuery) and pass them to children (client component) useThreadMessages hook?
  2. Let's say we're in chat-input.tsx. When the user submits a prompt, it creates a thread if we're not in one already by using the const createChat = useMutation(api.chat.createChat) that internally calls await createThread(ctx, components.agent). Is it possible to have an optimisticId to show the new message has been sent without waiting for the backend to process it? We want to redirect to the c/{chatId} page when user starts chatting...

"The best approach for now is to use preloadQuery to load just the first page, and separately have the actual usePaginatedQuery load the first page on the client.", how this would look like?

slender minnowBOT
#

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

#

Certainly! Here’s what the knowledge sources say about your two questions:


1. Preloading chat messages on SSR and passing to useThreadMessages

Currently, preloading chat messages with preloadQuery on the server and then passing them to the useThreadMessages hook in a client component is not directly supported. According to a Discord support thread, the recommended approach is:

  • Use preloadQuery to load just the first page of messages on the server (SSR).
  • On the client, use the actual usePaginatedQuery (or useThreadMessages) to load the first page again, ensuring reactivity and live updates.

This means you can't hydrate useThreadMessages directly with the preloaded data; instead, you use SSR to render the initial state, and then the client hook fetches/reactivates the data once mounted. This avoids hydration mismatches and ensures the client stays in sync with live updates.

"The best approach for now is to use preloadQuery to load just the first page, and separately have the actual usePaginatedQuery load the first page on the client."
Discord support thread: Preloading Next.js thread messages and then reading them with useThreadMessages


2. Optimistic IDs for new messages before backend processing

For optimistic UI updates (e.g., showing a message as "sent" before the backend confirms), you can use an optimistic update pattern. The Convex docs and Discord threads suggest:

  • When sending a message, generate a temporary (optimistic) ID on the client (e.g., using crypto.randomUUID()).
  • Add the optimistic message to your local state or query result.
  • Once the backend processes the message and returns the real ID, the optimistic message is replaced by the actual one.
#

For paginated queries (like chat), Convex now provides helpers such as insertAtTop for optimistic updates, but for useThreadMessages (which is built on paginated queries), you may need to manage a local array of optimistic messages and merge it with the paginated results in your component state.

Example optimistic update for a chat message:

const sendMessage = useMutation(api.messages.send).withOptimisticUpdate(
  (localStore, args) => {
    const { channel, body } = args;
    const existingMessages = localStore.getQuery(api.messages.list, { channel });
    if (existingMessages !== undefined) {
      const now = Date.now();
      const newMessage = {
        _id: crypto.randomUUID(),
        _creationTime: now,
        channel,
        body,
      };
      localStore.setQuery(api.messages.list, { channel }, [
        ...existingMessages,
        newMessage,
      ]);
    }
  }
);

Client > React > Optimistic Updates

For paginated queries, if you want to show optimistic messages, the recommended workaround is to keep a separate local state array for optimistic messages and merge it with the paginated results, removing optimistic messages once the real ones arrive. This is because you can't directly add to paginated query results with optimistic updates (unless you use the new helpers in Convex 1.24.0+ for paginated queries) Discord support thread: implement optimistic updates for a paginated query.


3. Example: SSR preload + client reactivity

SSR (Server Component):

const preloadedMessages = await preloadQuery(api.chat.listMessages, {
  threadId,
  paginationOpts: { numItems: 10, cursor: null },
});
#

Client Component:

const { results, status, loadMore } = useThreadMessages(
  api.chat.listMessages,
  { threadId },
  { initialNumItems: 10, stream: true }
);
// Merge with local optimistic messages if needed

You do not pass preloadedMessages directly to useThreadMessages. Instead, let the client hook fetch/reactivate the data, and manage optimistic messages in local state if you want to show them before backend confirmation.


Summary:

  • SSR preloading and client reactivity are separate; use SSR for initial render, then let the client hook fetch/reactivate.
  • For optimistic messages, use a local state array and merge with paginated results, or use new optimistic helpers if available for your query type.

If you need a more concrete code example for merging optimistic messages with paginated results, let me know!

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

#

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

distant rivet
#

@slender minnow show a concrete example.

First page.tsx (SSR side) then chat.tsx (client side)

slender minnowBOT
#

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

#

Certainly! Here’s a concrete example based on the current best practices from the knowledge sources for preloading chat messages on the server (SSR) and then using a client component for reactivity and optimistic updates.


1. page.tsx (Server Component, SSR)

This component preloads the first page of messages using preloadQuery and passes the thread ID to the client component. Note: You do not pass the preloaded data to the client hook; the client will fetch/reactivate the data itself.

// app/page.tsx (Server Component)
import { preloadQuery } from "convex/nextjs";
import { api } from "@/convex/_generated/api";
import Chat from "./chat";

export default async function Page({ params }) {
  const threadId = params.threadId;
  // Preload the first page of messages (for SSR)
  await preloadQuery(api.chat.listMessages, {
    threadId,
    paginationOpts: { numItems: 10, cursor: null },
  });
  // Pass threadId to the client component
  return <Chat threadId={threadId} />;
}

Discord support thread: Preloading Next.js thread messages and then reading them with useThreadMessages


2. chat.tsx (Client Component)

This component uses the useThreadMessages hook for reactivity and live updates. If you want to show optimistic messages, you can manage them in local state and merge them with the results.

// app/chat.tsx (Client Component)
"use client";

import { useState } from "react";
import { api } from "@/convex/_generated/api";
import { useThreadMessages, toUIMessages } from "@convex-dev/agent/react";

export default function Chat({ threadId }) {
  // Local state for optimistic messages
  const [optimisticMessages, setOptimisticMessages] = useState([]);
#

// Fetch paginated, reactive messages
const messages = useThreadMessages(
api.chat.listMessages,
{ threadId },
{ initialNumItems: 10, stream: true }
);

// Merge optimistic messages with fetched results
const allMessages = [
...optimisticMessages,
...(messages.results ?? [])
];

// Render messages
return (
<div>
{toUIMessages(allMessages).map((message) => (
<div key={message.key}>{message.text}</div>
))}
{/* ...input and send logic here... */}
</div>
);
}

[Agents > Messages: Showing messages in React](<https://docs.convex.dev/agents/messages#showing-messages-in-react>)

---

**Key Points:**
- SSR (`page.tsx`): Use `preloadQuery` for the initial page, but do not pass the result to the client hook.
- Client (`chat.tsx`): Use `useThreadMessages` for reactivity. For optimistic UI, manage local optimistic messages and merge them with the fetched results.

This approach follows the current best practice as described in the Discord support thread and the Convex documentation.
----------------------
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
----------------------
#

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

distant rivet
#

@slender minnow but chat.tsx receives preloadedMessages as prop from page.tsx?

slender minnowBOT
#

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

#

No, according to the current best practices and the information in the knowledge sources, the client component (chat.tsx) does not receive preloadedMessages as a prop from the server component (page.tsx).

Instead, the server component uses preloadQuery to ensure the first page of messages is fetched during SSR, but this data is not directly passed to the client component. The client component then uses useThreadMessages (or usePaginatedQuery) to fetch and subscribe to the messages reactively on the client side. This ensures live updates and avoids hydration mismatches.

This approach is explicitly recommended in the Discord support thread:

"The best approach for now is to use preloadQuery to load just the first page, and separately have the actual usePaginatedQuery load the first page on the client."
Discord support thread: Preloading Next.js thread messages and then reading them with useThreadMessages

So, in summary:

  • page.tsx uses preloadQuery for SSR, but does not pass the preloaded data to chat.tsx.
  • chat.tsx fetches/reactivates the data itself using useThreadMessages or similar hooks.

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

#

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

distant rivet
#

@slender minnow ""The best approach for now is to use preloadQuery to load just the first page, and separately have the actual usePaginatedQuery load the first page on the client."
" it's not what the example you gave does?

slender minnowBOT
#

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

#

You're correct to point out the distinction, and let's clarify with reference to the knowledge sources:

The approach described in the Discord thread is:

  • Use preloadQuery on the server to load the first page (for SSR and faster initial paint).
  • On the client, use the actual usePaginatedQuery (or useThreadMessages) to load/reactivate the first page again, ensuring reactivity and live updates.

The key point is:
You do not pass the result of preloadQuery (the preloaded data) as a prop to the client component's hook. Instead, you use preloadQuery for SSR, and then the client hook independently fetches/reactivates the data. This avoids hydration mismatches and ensures the client is fully reactive.

The example I gave matches this pattern:

  • The server component (page.tsx) calls preloadQuery but does not pass the preloaded data to the client.
  • The client component (chat.tsx) uses useThreadMessages to fetch/reactivate the data.

This is exactly what the Discord thread recommends:

"The best approach for now is to use preloadQuery to load just the first page, and separately have the actual usePaginatedQuery load the first page on the client."
Discord support thread: Preloading Next.js thread messages and then reading them with useThreadMessages

So, the example is consistent with the recommended approach:

  • SSR: Use preloadQuery for the initial load.
  • Client: Use the paginated/reactive hook to fetch and subscribe to updates, not the preloaded data.
#

If you want to preload and hydrate a non-paginated query, you can pass the preloaded result to usePreloadedQuery, but for paginated queries (like chat messages), the current best practice is to let the client hook fetch/reactivate the data itself.

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

#

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

distant rivet
#

@slender minnow so there won't be a flash of content?

slender minnowBOT
#

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

#

Based on the knowledge sources, using preloadQuery on the server and then letting the client load the first page again with usePaginatedQuery or useThreadMessages can still result in a flash of loading state or content. This is because the client-side hook does not hydrate from the preloaded data, but instead re-fetches/reactivates the data after hydration, which can cause a brief period where the UI shows a loading state or is empty.

This issue is discussed in the Discord thread, where a Convex staff member notes:

"The best approach for now is to use preloadQuery to load just the first page, and separately have the actual usePaginatedQuery load the first page on the client."
However, this approach does not guarantee that there will be no flash, as the client does not use the preloaded data for hydration and must fetch/reactivate the data itself, leading to a potential flash of loading state or empty content during the transition from SSR to client-side rendering Discord support thread: Preloading Next.js thread messages and then reading them with useThreadMessages.

Additionally, in another Discord thread, a Convex staff member mentions:

"The only flash left is the transition in Next.js from server-rendered content to live-refreshing (authed preloadQuery()) and I've got a fix for that coming, should be out this week."
This indicates that the flash is a known issue and that a more seamless hydration experience is being worked on, but as of the information in the sources, the flash can still occur Discord support thread: WorkOS Authkit as Custom Auth Integration with Convex.

#

In summary:
With the current approach, a flash of content or loading state is possible during the transition from SSR to client-side reactivity for paginated queries. This is a known limitation, and improvements are planned but not yet generally available according to the sources.

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

#

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