Docs

React UI

Persistence

Restore, store, reset, and sync React UI message state.

@anvia/react keeps UIMessage[] state in the browser. Your app decides whether that state is ephemeral, restored from local storage, or loaded from a server-side conversation store.

Initial messages

Use initialMessages when the page already has a transcript.

import type { UIMessage } from "@anvia/react";
import { useChat } from "@anvia/react";

export function ChatPage({ initialMessages }: { initialMessages: UIMessage[] }) {
  const chat = useChat({
    endpoint: "http://localhost:8787/api/chat",
    initialMessages,
  });

  return <ChatProvider controller={chat}>{/* thread */}</ChatProvider>;
}

Local persistence

Use local storage for prototypes or personal drafts. Production apps usually store transcripts on the server.

import type { UIMessage } from "@anvia/react";
import { useChat } from "@anvia/react";
import { useEffect, useMemo } from "react";

const storageKey = "support-chat";

export function LocalChat() {
  const initialMessages = useMemo<UIMessage[]>(() => {
    const stored = window.localStorage.getItem(storageKey);
    return stored === null ? [] : (JSON.parse(stored) as UIMessage[]);
  }, []);

  const chat = useChat({
    endpoint: "http://localhost:8787/api/chat",
    initialMessages,
  });

  useEffect(() => {
    window.localStorage.setItem(storageKey, JSON.stringify(chat.messages));
  }, [chat.messages]);

  return <ChatProvider controller={chat}>{/* thread */}</ChatProvider>;
}

Server persistence

A production route usually receives a thread id, authenticates the user, and stores messages or runtime events after the stream completes.

UIMessage.metadata is preserved by the supported conversion path, including Composer entities:

import { coreMessagesToUIMessages, uiMessagesToCoreMessages } from "@anvia/core/ui";

const coreMessages = uiMessagesToCoreMessages(uiMessages);
await conversationStore.save(JSON.parse(JSON.stringify(coreMessages)));

const restored = coreMessagesToUIMessages(await conversationStore.load());

Anvia’s SQLite, Postgres, Prisma, and Drizzle memory adapters store complete core messages, and Studio persists message metadata in its normalized SQLite tables. Provider adapters omit this UI metadata from model requests. Consumers no longer need to rewrite mentions into fake links or reattach Composer metadata after rehydration.

For memory-backed chats, load the saved core messages on the server with session.messages() and return them from your API as Anvia Message[]. The React page converts that payload with initialMessagesFromMemory(...) before passing it to useChat.

export async function GET(request: Request, params: { threadId: string }) {
  const user = await requireUser(request);
  const agent = createSupportAgent(user);

  const messages = await agent
    .session(params.threadId, {
      userId: user.id,
      metadata: { tenantId: user.tenantId },
    })
    .messages();

  return Response.json({ messages });
}
import type { Message } from "@anvia/core/completion";
import { initialMessagesFromMemory, useChat } from "@anvia/react";

export function ServerBackedChat({
  threadId,
  messages,
}: {
  threadId: string;
  messages: Message[];
}) {
  const chat = useChat({
    endpoint: `http://localhost:8787/api/conversations/${threadId}/chat`,
    initialMessages: initialMessagesFromMemory(messages),
  });

  return <ChatProvider controller={chat}>{/* thread */}</ChatProvider>;
}
const chat = useChat({
  endpoint: "http://localhost:8787/api/chat",
  initialMessages,
  createRequest: ({ coreMessages }) => ({
    threadId,
    messages: coreMessages,
    stream: true,
  }),
});
export async function POST(request: Request) {
  const user = await requireUser(request);
  const body = await parseChatRequest(request);
  const thread = await requireThread(user, body.threadId);

  return createEventStream(
    streamAndPersist({
      events: agent.prompt(body.messages).stream(),
      thread,
    }),
  );
}

When the server route is backed by agent.session(...), treat initialMessages as browser hydration. On the POST route, run only the latest user message through the session prompt; the server-side MemoryStore loads prior history. See Sessions and memory for a complete route example.

Reset

Use reset() for a new conversation:

<button type="button" onClick={() => chat.reset()}>
  New chat
</button>

Use reset(messages) after loading another thread:

const loaded = await loadThread(threadId);
chat.reset(loaded.messages);

setMessages

Use setMessages for local edits such as deleting a draft or trimming a transcript.

chat.setMessages((messages) => messages.filter((message) => message.id !== deletedMessageId));

Do not use browser-side message editing as the only source of truth for audited production conversations. Store authoritative history on the server.