The Problem
In a typical chat application, creating a new chat requires:- User submits a message
- Server creates a chat record and returns an ID
- Client navigates to
/chat/:id - Page reloads, components remount, state is lost
The Solution
ChatJS generates a provisional UUID on the client before the user sends any message. When the message is submitted, the chat is persisted with that same ID. Because the ID never changes, React components do not remount and the transition is seamless.How It Works
1. ChatIdProvider Generates Provisional IDs
TheChatIdProvider creates a provisional UUID when a user visits the homepage:
/chat/abc123), that ID is used. Otherwise, the provisional ID is returned:
2. ChatSystem Uses ID as React Keys
TheChatSystem component uses the chat ID as React keys for its providers:
3. URL Updates Without Navigation
After the chat is persisted, the URL is updated using the History API:4. New Chat Resets State
When starting a new chat, buttons likeNewChatButton and SidebarTopRow call refreshChatID:
5. Server Confirms Persistence
On new chat creation, the server emits adata-chatConfirmed event:
DataStreamHandler only confirms the chat ID if it matches the current provisional ID, preventing stale confirmations from affecting newer chats:
The Flow
- Visit homepage (
/) —ChatIdProvidergenerates provisional IDabc123 - User types message — ID is still
abc123(provisional) - User submits message — Chat saved to database with ID
abc123 - Server confirms —
data-chatConfirmedevent emitted (only on new chats) - Client validates —
DataStreamHandlercheckschatIdmatches current provisional ID before confirming - URL updates — Changes to
/chat/abc123via History API (no reload) - Components stay mounted — Key unchanged, no remount
- User clicks “New Chat” —
refreshChatIDgenerates new ID, components remount with fresh state
Key Components
| Component | Responsibility |
|---|---|
ChatIdProvider | Generates and manages provisional/confirmed chat IDs |
ChatSystem | Uses ID as React key to control component lifecycle |
DataStreamHandler | Listens for data-chatConfirmed and validates ID match before confirming |
ChatInputProvider | Manages input state with localStorage persistence |
NewChatButton | Calls refreshChatID to start fresh |
SidebarTopRow | Calls refreshChatID when logo is clicked |
Benefits
- No page flicker: The transition from provisional to persisted is invisible to users
- Preserved state: Input attachments, model selection, and scroll position remain intact
- Predictable IDs: The chat ID is known before persistence, enabling optimistic UI patterns
- Clean separation: New chats get fresh state through React key changes
Related
- URL Routing for pathname parsing utilities
- Resumable Streams for handling page refreshes during streaming