useMessageSync
useMessageSync subscribes to view updates and syncs them into Vercel AI SDK's useChat overlay through the setMessages updater. It is how observer clients (other tabs, other devices) catch up with messages that arrive on the channel rather than from useChat's own sendMessage calls.
During an active own-run stream the sync is gated so it does not race the AI SDK's internal write() and produce an ID mismatch. When the stream ends, the gate opens and the next view update is merged in. Tool resolutions in the local overlay that have not yet echoed back through the channel are preserved during the merge.
1
2
3
4
5
6
7
8
9
10
11
import { useChat } from '@ai-sdk/react';
import { useChatTransport, useMessageSync } from '@ably/ai-transport/vercel/react';
function Chat() {
const { chatTransport } = useChatTransport();
const { messages, setMessages, sendMessage } = useChat({ transport: chatTransport });
useMessageSync({ setMessages });
return <Conversation messages={messages} onSend={sendMessage} />;
}This hook must be used within a ChatTransportProvider (exported from @ably/ai-transport/vercel/react) unless channelName resolves to a different registered provider.
Parameters
setMessagesrequired(updater: (prev: UIMessage[]) => UIMessage[]) => voidsetMessages updater function from useChat(). Called with an updater that returns the next overlay.channelNameoptionalStringChatTransportProvider to observe. Omit to use the nearest provider.skipoptionalBooleantrue, skip all subscriptions.Returns
void. The hook subscribes for its side effects and cleans up on unmount.
How the sync works
The hook does two things:
- Subscribes to the
ChatTransport'sonStreamingChangeevent. Whilestreamingistrue(own-run stream consumed byuseChat), the gate is closed and nosetMessagescalls are made. - Subscribes to the underlying
View's'update'event. When the view changes and the gate is open, the hook callssetMessageswith an updater that merges the view's messages into the overlay.
The merge is per message, not a wholesale replace. For each assistant message, the merge preserves any dynamic-tool part in the overlay whose state is output-available, output-error, approval-responded, or output-denied while the tree's part is still in an unresolved state. That keeps local addToolResult and addToolApprovalResponse actions visible until the channel echo lands.
When to use it
Use useMessageSync whenever you mount a ChatTransportProvider and render messages through useChat. Without it, observer tabs or devices that did not originate a Run never see its messages because useChat's state is local to each useChat instance.
If you render messages directly from useView instead of from useChat, you do not need useMessageSync; useView already reflects the view state.
Example
A complete Vercel-React chat with sync wired in. A second browser tab pointing at the same channelName sees every message as soon as it arrives on the channel.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { useChat } from '@ai-sdk/react';
import { useChatTransport, useMessageSync } from '@ably/ai-transport/vercel/react';
function Chat() {
const { chatTransport, chatTransportError } = useChatTransport();
const { messages, setMessages, sendMessage, status } = useChat({
transport: chatTransport,
});
useMessageSync({ setMessages });
if (chatTransportError) return <ErrorBanner error={chatTransportError} />;
return (
<>
{messages.map((m) => <Message key={m.id} message={m} />)}
<Composer
disabled={status !== 'ready'}
onSubmit={(text) =>
sendMessage({ role: 'user', parts: [{ type: 'text', text }] })
}
/>
</>
);
}