Skip to content

Commit 87de56b

Browse files
ttypicclaude
andcommitted
react: add TransportProvider, make useClientTransport a context reader
Previously, useClientTransport was a factory hook: callers were responsible for obtaining a channel via useChannel, passing it along with codec and other options, and threading the resulting transport instance through props to every consumer component. This change introduces TransportProvider<TEvent, TMessage>, which centralises channel setup and transport creation. The provider wraps ChannelProvider internally, co-locates channel and transport lifecycle, and registers the transport in a React context. useClientTransport is now a thin context reader. - src/react/contexts/transport-context.ts: new context (Record-based) - src/react/contexts/transport-provider.tsx: new provider, two-component pattern so useChannel is called inside ChannelProvider's subtree - src/react/use-client-transport.ts: rewritten as context reader - Both demos updated to wrap at page level with TransportProvider - All docs updated to the new API - Tests rewritten: transport-provider.test.ts (new, 8 tests), use-client-transport.test.ts (context-based, no transport creation) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 173e87b commit 87de56b

22 files changed

Lines changed: 750 additions & 154 deletions

README.md

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -143,15 +143,13 @@ export async function POST(req: Request) {
143143
'use client';
144144

145145
import { useChat } from '@ai-sdk/react';
146-
import { useChannel } from 'ably/react';
147-
import { useClientTransport, useActiveTurns, useView } from '@ably/ai-transport/react';
146+
import type * as AI from 'ai';
147+
import { TransportProvider, useClientTransport, useActiveTurns, useView } from '@ably/ai-transport/react';
148148
import { useChatTransport, useMessageSync } from '@ably/ai-transport/vercel/react';
149149
import { UIMessageCodec } from '@ably/ai-transport/vercel';
150150

151-
function Chat({ chatId, clientId }: { chatId: string; clientId?: string }) {
152-
const { channel } = useChannel({ channelName: chatId });
153-
154-
const transport = useClientTransport({ channel, codec: UIMessageCodec, clientId });
151+
function ChatInner({ chatId }: { chatId: string }) {
152+
const transport = useClientTransport<AI.UIMessageChunk, AI.UIMessage>();
155153
const chatTransport = useChatTransport(transport);
156154

157155
const { messages, setMessages, sendMessage, stop } = useChat({
@@ -162,7 +160,7 @@ function Chat({ chatId, clientId }: { chatId: string; clientId?: string }) {
162160
useMessageSync(transport, setMessages);
163161

164162
const activeTurns = useActiveTurns(transport);
165-
const view = useView(transport, { limit: 30 });
163+
useView(transport, { limit: 30 });
166164

167165
return (
168166
<div>
@@ -176,19 +174,22 @@ function Chat({ chatId, clientId }: { chatId: string; clientId?: string }) {
176174
}}
177175
>
178176
{activeTurns.size > 0 ? (
179-
<button
180-
type="button"
181-
onClick={stop}
182-
>
183-
Stop
184-
</button>
177+
<button type="button" onClick={stop}>Stop</button>
185178
) : (
186179
<button type="submit">Send</button>
187180
)}
188181
</form>
189182
</div>
190183
);
191184
}
185+
186+
function Chat({ chatId, clientId }: { chatId: string; clientId?: string }) {
187+
return (
188+
<TransportProvider channelName={chatId} codec={UIMessageCodec} clientId={clientId}>
189+
<ChatInner chatId={chatId} />
190+
</TransportProvider>
191+
);
192+
}
192193
```
193194

194195
### Authentication

ably-common

demo/vercel/react/use-chat/src/app/chat.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
'use client';
22

33
import { useChat } from '@ai-sdk/react';
4-
import { useChannel } from 'ably/react';
4+
import type * as AI from 'ai';
55
import { useClientTransport, useActiveTurns, useView, useAblyMessages } from '@ably/ai-transport/react';
66
import { useChatTransport, useMessageSync } from '@ably/ai-transport/vercel/react';
7-
import { UIMessageCodec } from '@ably/ai-transport/vercel';
87
import { useState } from 'react';
98
import { MessageList } from './components/message-list';
109
import { DebugPane } from './components/debug-pane';
@@ -15,10 +14,8 @@ import { useClientTools } from './hooks/use-client-tools';
1514
// ---------------------------------------------------------------------------
1615

1716
export function Chat({ chatId, clientId, historyLimit }: { chatId: string; clientId?: string; historyLimit?: number }) {
18-
const { channel } = useChannel({ channelName: chatId });
19-
20-
// Create transport immediately (subscribes before attach — RTL7g)
21-
const transport = useClientTransport({ channel, codec: UIMessageCodec, clientId, body: () => ({ id: chatId }) });
17+
// Transport is created by TransportProvider in page.tsx
18+
const transport = useClientTransport<AI.UIMessageChunk, AI.UIMessage>();
2219
const chatTransport = useChatTransport(transport);
2320

2421
const {

demo/vercel/react/use-chat/src/app/page.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
'use client';
22

33
import { Providers, useAblyReady } from './providers';
4-
import { ChannelProvider } from 'ably/react';
4+
import { TransportProvider } from '@ably/ai-transport/react';
5+
import { UIMessageCodec } from '@ably/ai-transport/vercel';
56
import { Chat } from './chat';
67
import { useSearchParams } from 'next/navigation';
78
import { Suspense } from 'react';
@@ -16,13 +17,17 @@ function ChatWhenReady({ channelName, clientId, limit }: { channelName: string;
1617
}
1718

1819
return (
19-
<ChannelProvider channelName={channelName}>
20+
<TransportProvider
21+
channelName={channelName}
22+
codec={UIMessageCodec}
23+
clientId={clientId}
24+
>
2025
<Chat
2126
chatId={channelName}
2227
clientId={clientId}
2328
historyLimit={limit}
2429
/>
25-
</ChannelProvider>
30+
</TransportProvider>
2631
);
2732
}
2833

demo/vercel/react/use-client-transport/src/app/components/chat.tsx

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
'use client';
22

33
import { useState, useCallback } from 'react';
4-
import { useChannel } from 'ably/react';
4+
import type * as AI from 'ai';
55
import { useClientTransport, useCreateView, useActiveTurns, useView, useAblyMessages } from '@ably/ai-transport/react';
6-
import { UIMessageCodec } from '@ably/ai-transport/vercel';
76

87
import { userMessage } from '../helpers';
98
import { useClientTools } from '../hooks/use-client-tools';
@@ -30,16 +29,10 @@ interface ChatProps {
3029
}
3130

3231
export function Chat({ chatId, clientId, historyLimit }: ChatProps) {
33-
const { channel } = useChannel({ channelName: chatId });
32+
// Transport is created by TransportProvider in page.tsx
33+
const transport = useClientTransport<AI.UIMessageChunk, AI.UIMessage>(chatId);
3434
const [split, setSplit] = useState(false);
3535

36-
const transport = useClientTransport({
37-
channel,
38-
codec: UIMessageCodec,
39-
clientId,
40-
body: () => ({ id: chatId }),
41-
});
42-
4336
const limit = historyLimit ?? 30;
4437
const view = useView(transport, { limit });
4538
const splitView = useCreateView(split ? transport : undefined, { limit });

demo/vercel/react/use-client-transport/src/app/page.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
'use client';
22

33
import { Providers, useAblyReady } from './providers';
4-
import { ChannelProvider } from 'ably/react';
4+
import { TransportProvider } from '@ably/ai-transport/react';
5+
import { UIMessageCodec } from '@ably/ai-transport/vercel';
56
import { Chat } from './components/chat';
67
import { useSearchParams } from 'next/navigation';
78
import { Suspense } from 'react';
@@ -16,13 +17,18 @@ function ChatWhenReady({ channelName, clientId, limit }: { channelName: string;
1617
}
1718

1819
return (
19-
<ChannelProvider channelName={channelName}>
20+
<TransportProvider
21+
channelName={channelName}
22+
codec={UIMessageCodec}
23+
clientId={clientId}
24+
body={() => ({ id: channelName })}
25+
>
2026
<Chat
2127
chatId={channelName}
2228
clientId={clientId}
2329
historyLimit={limit}
2430
/>
25-
</ChannelProvider>
31+
</TransportProvider>
2632
);
2733
}
2834

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
{
22
"compilerOptions": {
33
"target": "ES2017",
4-
"lib": ["dom", "dom.iterable", "esnext"],
4+
"lib": [
5+
"dom",
6+
"dom.iterable",
7+
"esnext"
8+
],
59
"allowJs": true,
610
"skipLibCheck": true,
711
"strict": true,
@@ -11,17 +15,27 @@
1115
"moduleResolution": "bundler",
1216
"resolveJsonModule": true,
1317
"isolatedModules": true,
14-
"jsx": "react-jsx",
18+
"jsx": "preserve",
1519
"incremental": true,
1620
"plugins": [
1721
{
1822
"name": "next"
1923
}
2024
],
2125
"paths": {
22-
"@/*": ["./src/*"]
26+
"@/*": [
27+
"./src/*"
28+
]
2329
}
2430
},
25-
"include": ["**/*.ts", "**/*.tsx", "next-env.d.ts", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],
26-
"exclude": ["node_modules"]
31+
"include": [
32+
"**/*.ts",
33+
"**/*.tsx",
34+
"next-env.d.ts",
35+
".next/types/**/*.ts",
36+
".next/dev/types/**/*.ts"
37+
],
38+
"exclude": [
39+
"node_modules"
40+
]
2741
}

docs/concepts/transport.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,20 @@ view.on('update', () => {
7777
// (e.g. Vercel's useChat), but most apps use the view instead
7878
```
7979

80-
In React, the hooks handle subscriptions and state management:
80+
In React, `TransportProvider` creates the transport and `useClientTransport` reads it from context:
8181

8282
```typescript
83-
import { useClientTransport, useView } from '@ably/ai-transport/react';
83+
import { TransportProvider, useClientTransport, useView } from '@ably/ai-transport/react';
8484
import { UIMessageCodec } from '@ably/ai-transport/vercel';
85+
import type * as AI from 'ai';
8586

86-
const transport = useClientTransport({ channel, codec: UIMessageCodec, clientId });
87+
// In your layout or page component:
88+
<TransportProvider channelName="ai:demo" codec={UIMessageCodec} clientId={clientId}>
89+
<Chat />
90+
</TransportProvider>
91+
92+
// Inside Chat:
93+
const transport = useClientTransport<AI.UIMessageChunk, AI.UIMessage>();
8794
const { nodes, send } = useView(transport);
8895
```
8996

docs/features/multi-client.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,16 @@ All clients subscribe to the same Ably channel. The transport distinguishes betw
1313

1414
No special API is needed. Connect two clients to the same channel name, and messages sync automatically:
1515

16-
```typescript
17-
// Client A
18-
const transportA = useClientTransport({ channel, codec: UIMessageCodec, clientId: 'user-a' });
19-
20-
// Client B (different browser tab, device, or user)
21-
const transportB = useClientTransport({ channel, codec: UIMessageCodec, clientId: 'user-b' });
16+
```tsx
17+
// Client A — in its own browser tab
18+
<TransportProvider channelName="ai:demo" codec={UIMessageCodec} clientId="user-a">
19+
<Chat />
20+
</TransportProvider>
21+
22+
// Client B — in a different browser tab, device, or user session
23+
<TransportProvider channelName="ai:demo" codec={UIMessageCodec} clientId="user-b">
24+
<Chat />
25+
</TransportProvider>
2226

2327
// When Client A sends a message and the server streams a response,
2428
// Client B sees both the user message and the assistant response

docs/frameworks/vercel-ai-sdk.md

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,20 @@ The Vercel AI SDK provides model abstraction, streaming primitives, and React ho
1919

2020
Wrap the transport in a `ChatTransport` adapter and pass it to Vercel's `useChat()`. Message state is managed by `useChat()` - the transport delivers messages over Ably instead of HTTP.
2121

22-
```typescript
23-
import { useClientTransport } from '@ably/ai-transport/react';
22+
```tsx
23+
import { TransportProvider, useClientTransport } from '@ably/ai-transport/react';
2424
import { useChatTransport, useMessageSync } from '@ably/ai-transport/vercel/react';
2525
import { UIMessageCodec } from '@ably/ai-transport/vercel';
2626
import { useChat } from '@ai-sdk/react';
27+
import type * as AI from 'ai';
28+
29+
// Wrap your component tree with TransportProvider
30+
<TransportProvider channelName={chatId} codec={UIMessageCodec} clientId={clientId}>
31+
<ChatInner chatId={chatId} />
32+
</TransportProvider>
2733

28-
const transport = useClientTransport({ channel, codec: UIMessageCodec, clientId });
34+
// Inside ChatInner:
35+
const transport = useClientTransport<AI.UIMessageChunk, AI.UIMessage>();
2936
const chatTransport = useChatTransport(transport);
3037

3138
const { messages, setMessages, sendMessage, stop } = useChat({
@@ -37,17 +44,28 @@ const { messages, setMessages, sendMessage, stop } = useChat({
3744
useMessageSync(transport, setMessages);
3845
```
3946

40-
`useChatTransport()` wraps the core transport into the `ChatTransport` interface that `useChat()` expects. `useMessageSync()` pushes the transport's authoritative message list into `useChat()`'s state - this is how messages from other clients appear.
47+
`TransportProvider` creates the transport and wraps children with `ChannelProvider` internally. `useClientTransport()` reads it from context. `useChatTransport()` wraps the core transport into the `ChatTransport` interface that `useChat()` expects. `useMessageSync()` pushes the transport's authoritative message list into `useChat()`'s state - this is how messages from other clients appear.
4148

4249
### Generic hooks path (more control)
4350

4451
Use the generic React hooks directly. You manage message state through the transport's conversation tree instead of `useChat()`.
4552

46-
```typescript
47-
import { useClientTransport, useView, useActiveTurns } from '@ably/ai-transport/react';
53+
```tsx
54+
import {
55+
TransportProvider,
56+
useClientTransport,
57+
useView,
58+
useActiveTurns} from '@ably/ai-transport/react';
4859
import { UIMessageCodec } from '@ably/ai-transport/vercel';
60+
import type * as AI from 'ai';
61+
62+
// Wrap your component tree with TransportProvider
63+
<TransportProvider channelName={chatId} codec={UIMessageCodec} clientId={clientId}>
64+
<ChatInner />
65+
</TransportProvider>
4966

50-
const transport = useClientTransport({ channel, codec: UIMessageCodec, clientId });
67+
// Inside ChatInner:
68+
const transport = useClientTransport<AI.UIMessageChunk, AI.UIMessage>();
5169
const {
5270
nodes,
5371
hasOlder,

0 commit comments

Comments
 (0)