#Optimistically mutating data

13 messages · Page 1 of 1 (latest)

faint jolt
#

I have a chatbot. When I send my first message to a new channel and optimisitically update that initial message send, Getting the existing messages from the localstore returns undefined
const existingMessages = localStorage.getQuery(api.chat.getMessages, {
conversationId,
});

Is this expected? This is the first message to the channel so it makes sense that the channel would have no messages, but why doesn't it return an empty array? I want to be able to optimistically update the UI on that initial message. Subsequent messages do optimisitically update. It is just the first one where the existingMessages query to the localstorage is undefined so we don't optimistically update

simple fogBOT
#

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

#

Yes, this is expected behavior in Convex. When you use localStore.getQuery(...) inside an optimistic update, it will return undefined if the query has not yet been loaded into the client. This typically happens when you are sending the very first message to a new channel or conversation—since there are no messages yet, the query has never been run, so there is no cached result (not even an empty array).

From the documentation:

The useQuery React hook will return undefined when it is first mounted, before the query has been loaded from Convex. Once a query is loaded it will never be undefined again (even as the data reactively updates). undefined is not a valid return type for queries... You can use this as a signal for when to render loading indicators and placeholder UI.
Best-practices: UI patterns

And for optimistic updates:

This optimistic update changes the api.messages.list query for the current channel to include a new message. ... If we've loaded the api.messages.list query, push an optimistic message onto the list.
Optimistic Updates: Complex Example

So, in your code:

const existingMessages = localStore.getQuery(api.chat.getMessages, {
  conversationId,
});

If this is the first message, existingMessages will be undefined. For subsequent messages, once the query has been loaded (even if it returns an empty array), getQuery will return the array (possibly empty), and your optimistic update will work as expected.

How to handle this:
You can check for undefined and treat it as an empty array when applying your optimistic update:

#
const existingMessages = localStore.getQuery(api.chat.getMessages, {
  conversationId,
}) || [];
// Now you can optimistically add the new message
localStore.setQuery(api.chat.getMessages, { conversationId }, [
  ...existingMessages,
  newMessage,
]);

This pattern ensures that your optimistic update works even for the very first message in a new channel or conversation.

Summary:

  • getQuery returns undefined if the query hasn't been loaded yet (e.g., first message).
  • Treat undefined as an empty array for optimistic updates.
  • This is the recommended pattern in Convex for handling initial optimistic updates.
    Optimistic Updates

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

#

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

faint jolt
#

@simple fog Are there any downsides to this? Could we ever get in a broken state because of this?

simple fogBOT
#

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

#

There are no documented downsides or risks of getting into a broken state by treating an initial undefined from localStore.getQuery as an empty array for optimistic updates. In fact, this is the recommended approach in Convex’s official documentation and examples. The Convex client is designed to roll back optimistic updates if the mutation fails or if the server returns different data, ensuring eventual consistency and preventing broken states:

  • Optimistic updates are temporary, local changes. If there are small mistakes in optimistic updates, the UI will always eventually render the correct values after the mutation completes and the queries are updated.
  • The Convex client will handle rolling back this update after the mutation completes and the queries are updated.
  • Always create new objects inside of optimistic updates to avoid corrupting the client’s internal state.
    Optimistic Updates: Complex Example

So, as long as you follow the pattern of treating undefined as an empty array and avoid mutating existing objects (always create new arrays/objects), you should not encounter broken states. The system is designed to be robust against these scenarios.

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

#

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

faint jolt
#

@simple fog Sweet. I have another question. Is it possible to optimisticially update my UI when I create a new chat? Upon first message I create a chat and send the message to the new chat optimisitically. The message updates are snappy because they are optimistic, but I'm unsure how to optimistically create the chat becasue the messages depend on the chat id. We also use the chat id for our URL as you can see
const onSubmit = async () => {
const tempQuery = query;
if (!tempQuery.trim()) return;

// Clear out the query
setQuery("");

// Create new conversation if none exist
var tempConversationId = conversationId;
if (!tempConversationId) {
  try {
    // TODO: Should we bundle this functionality into sendMessage?
    // We could immediately optimistically update the ui,
    // or maybe even generate the uuid on the client and send it up so that is even more instant
    tempConversationId = await createConversation({
      title: query.slice(0, 50) + (query.length > 50 ? "..." : ""),
      model: "",
    });
  } catch (error) {
    console.error("Failed to create conversation:", error);
    // Restore the query on failure
    setQuery(tempQuery);
    return;
  }
  // TODO: Stop loading
}

try {
  await sendMessage({
    conversationId: tempConversationId as Id<"conversations">,
    content: tempQuery,
  });

  // If a new chat was created we redirect
  if (!conversationId) {
    router.replace(`/chat/${tempConversationId}`);
  }
} catch (error) {
  console.error("Failed to send message:", error);
  setQuery(tempQuery); // Restore message on failure
}

};

simple fogBOT
#

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

#

Currently, Convex does not support client-side generation of document IDs (such as a chat/conversation ID) that can be used both for optimistic updates and as a stable identifier for things like URLs. This limitation is a known pain point for use cases like yours, where you want to optimistically create a chat and immediately use its ID for routing and message association.

What you can do:

  • The recommended workaround is to create your own custom ID field (e.g., clientId) and use that for optimistic updates and UI state. You can generate this ID on the client, use it for optimistic updates, and then reconcile it with the server-generated _id once the mutation completes. However, this approach requires extra logic to map between the client-generated ID and the server-assigned _id, and can introduce complexity, especially if you use the _id in URLs or as React keys. This workaround is discussed in community threads and acknowledged as less than ideal by Convex staff Discord: Setting _id when creating documents with optimistic update, Legend-State plugin for local-first.

  • As of now, you cannot pass a custom _id to Convex mutations to have the server use your client-generated ID. The Convex team has stated that client-side ID generation is on their roadmap and may be addressed in the future as part of a "local sync engine," but it is not available yet.

  • For now, you can optimistically update the UI for the messages themselves (as you are already doing), but you cannot optimistically update the UI for the chat/conversation in a way that uses a stable, server-accepted ID for routing until the server responds with the created chat's _id.

#

Summary:

  • You can optimistically update messages, but not the chat/conversation itself with a stable ID for routing.
  • Workarounds exist (custom client ID fields), but they add complexity and are not first-class in Convex.
  • Client-side ID generation for documents is not yet supported, but is planned for the future.

If you need to avoid UI "flashes" or delays, you may need to implement a temporary local state for the chat and reconcile it once the server responds, but this will require extra logic and careful handling of ID mapping.
Discord: Setting _id when creating documents with optimistic update
Legend-State plugin for local-first

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