Browser chat persistence on useChat / ChatClient: localStoragePersistence, sessionStoragePersistence, indexedDBPersistence. Client-authoritative (adapter, full transcript) vs server-authoritative (persistence: true, no client cache). Reload restore, pending interrupts, mid-stream rejoin with delivery durability. Use for SPA reload durability — NOT server history alone. No extra package: the adapters ship in the framework packages.
Builds on ai-core, and on
ai-core/chat-experienceforuseChatitself.No extra package. The adapters below ship in the framework packages (
@tanstack/ai-reactand friends, re-exported from@tanstack/ai-client), so browser persistence needs nothing installed beyond what a chat UI already has. The server half is a separate package — see@tanstack/ai-persistenceand itsai-persistence/serverskill.
A ChatClient / useChat keeps messages in memory. The persistence option
stores one record per threadId so a reload can repaint the transcript,
restore a pending interrupt, and rejoin an in-flight run.
Import adapters from the framework package (not @tanstack/ai-client
unless vanilla JS):
import {
useChat,
fetchServerSentEvents,
localStoragePersistence,
sessionStoragePersistence,
indexedDBPersistence,
} from '@tanstack/ai-react'| Adapter | Survives | Notes |
|---|---|---|
localStoragePersistence() | Reloads + browser restarts | Sync hydrate; quota-bound; JSON codec default |
sessionStoragePersistence() | Reloads in the same tab | Cleared when tab/session ends |
indexedDBPersistence() | Reloads + restarts | Async open (first paint may be empty briefly); structured clone |
All default to the chat persisted-state shape — no type argument or codec required for normal use.
function Chat() {
const { messages, sendMessage } = useChat({
threadId: 'support-chat', // stable — required
connection: fetchServerSentEvents('/api/chat'),
persistence: localStoragePersistence(),
})
// ...
}Bare adapter ≡ full transcript + resume pointer. Browser owns history; server
(if any) mirrors when you post non-empty messages.
Best for: SPA, offline-first, single device, moderate conversation size.
persistence: true)function Chat({ threadId }: { threadId: string }) {
const { messages, sendMessage } = useChat({
threadId,
connection: fetchServerSentEvents('/api/chat'),
persistence: true,
})
// ...
}Nothing is cached client-side: no transcript, no resume pointer.
On mount, useChat hydrates the thread from the server by threadId
(paint + tail active run). Same path for another device. Pair with server
withPersistence + a hydrate route (reconstructChat or equivalent).
Best for: large transcripts, multi-device, compliance (no message bodies in browser storage).
toServerSentEventsResponse(stream, { durability: … })) so the client can
joinRun and finish the reply. Persistence alone is not enough.threadId is the identityPersistence keys on threadId. The hooks have no separate id option — a
chat's identity is its threadId. Without a stable one, each load is a new
chat. Generate it server-side or from a route param the user owns; do not
randomize per mount.
threadIdRecord cannot be found after reload.
id to useChatRemoved — threadId is the identity. (ChatClient still accepts id directly
as a lower-level escape hatch for keying storage separately from the wire
thread; the framework hooks do not.)
persistence: true without server historyEmpty chat after reload unless the server can reconstruct by threadId.
localStorageQuota and main-thread cost. Prefer persistence: true + server store, or
IndexedDB with care.
localStorage is per-browser. Use server persistence for multi-device.
@tanstack/ai-persistence) — authoritative server halfuseChat, resumable connections1120f0f
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.