0
# Subscription Hooks
1
2
React hooks for real-time data subscriptions with automatic connection management, error handling, and reconnection logic. These hooks are automatically generated for each subscription procedure in your tRPC router.
3
4
## Capabilities
5
6
### useSubscription
7
8
Primary hook for establishing real-time subscriptions to tRPC subscription procedures.
9
10
```typescript { .api }
11
/**
12
* Hook for subscribing to real-time data streams from tRPC subscription procedures
13
* @param input - Input parameters for the subscription procedure
14
* @param opts - Subscription configuration options
15
* @returns Subscription result with current data and connection state
16
*/
17
procedure.useSubscription(
18
input: TInput,
19
opts?: UseTRPCSubscriptionOptions<TOutput, TError>
20
): TRPCSubscriptionResult<TOutput, TError>;
21
22
// Overload with skip token support
23
procedure.useSubscription(
24
input: TInput | SkipToken,
25
opts?: Omit<UseTRPCSubscriptionOptions<TOutput, TError>, 'enabled'>
26
): TRPCSubscriptionResult<TOutput, TError>;
27
28
interface UseTRPCSubscriptionOptions<TOutput, TError> {
29
/** Whether the subscription is enabled */
30
enabled?: boolean;
31
32
/** Callback fired when subscription receives data */
33
onData?: (data: TOutput) => void;
34
35
/** Callback fired when subscription starts */
36
onStarted?: () => void;
37
38
/** Callback fired on subscription errors */
39
onError?: (error: TError) => void;
40
41
/** Callback fired when subscription stops */
42
onStopped?: () => void;
43
44
/** tRPC-specific request options */
45
trpc?: TRPCReactRequestOptions;
46
}
47
48
interface TRPCSubscriptionResult<TData, TError> {
49
/** Current subscription data */
50
data: TData | undefined;
51
52
/** Subscription error if any */
53
error: TError | null;
54
55
/** Current subscription status */
56
status: 'idle' | 'connecting' | 'connected' | 'reconnecting' | 'stopped' | 'error';
57
58
/** tRPC-specific hook metadata */
59
trpc: TRPCHookResult;
60
}
61
62
type TRPCSubscriptionConnectingResult<TData, TError> = TRPCSubscriptionResult<TData, TError> & {
63
status: 'connecting';
64
};
65
66
type TRPCSubscriptionIdleResult<TData, TError> = TRPCSubscriptionResult<TData, TError> & {
67
status: 'idle';
68
};
69
```
70
71
**Usage Examples:**
72
73
```typescript
74
import { trpc } from "./utils/trpc";
75
76
function LiveNotifications({ userId }: { userId: number }) {
77
const { data, error, status } = trpc.notifications.subscribe.useSubscription(
78
{ userId },
79
{
80
onData: (notification) => {
81
console.log("New notification:", notification);
82
// Show toast notification
83
showNotification(notification.message);
84
},
85
onError: (error) => {
86
console.error("Subscription error:", error);
87
},
88
onStarted: () => {
89
console.log("Subscription started");
90
},
91
onStopped: () => {
92
console.log("Subscription stopped");
93
},
94
}
95
);
96
97
return (
98
<div>
99
<div>Status: {status}</div>
100
{error && <div>Error: {error.message}</div>}
101
{data && (
102
<div>
103
<h3>Latest Notification</h3>
104
<p>{data.message}</p>
105
<small>{new Date(data.timestamp).toLocaleString()}</small>
106
</div>
107
)}
108
</div>
109
);
110
}
111
```
112
113
### Connection Status Management
114
115
Monitor and handle different subscription connection states.
116
117
```typescript
118
function ConnectionStatusExample({ roomId }: { roomId: string }) {
119
const subscription = trpc.chat.subscribe.useSubscription(
120
{ roomId },
121
{
122
onData: (message) => {
123
console.log("New message:", message);
124
},
125
}
126
);
127
128
const renderConnectionStatus = () => {
129
switch (subscription.status) {
130
case 'idle':
131
return <div className="status idle">Not connected</div>;
132
case 'connecting':
133
return <div className="status connecting">Connecting...</div>;
134
case 'connected':
135
return <div className="status connected">Connected</div>;
136
case 'reconnecting':
137
return <div className="status reconnecting">Reconnecting...</div>;
138
case 'error':
139
return <div className="status error">Connection error</div>;
140
case 'stopped':
141
return <div className="status stopped">Disconnected</div>;
142
default:
143
return null;
144
}
145
};
146
147
return (
148
<div>
149
{renderConnectionStatus()}
150
{subscription.error && (
151
<div>Error: {subscription.error.message}</div>
152
)}
153
{subscription.data && (
154
<div>Latest message: {subscription.data.content}</div>
155
)}
156
</div>
157
);
158
}
159
```
160
161
### Conditional Subscriptions
162
163
Control when subscriptions are active using the enabled option or skip token.
164
165
```typescript
166
function ConditionalSubscription({ userId, isOnline }: { userId: number; isOnline: boolean }) {
167
// Using enabled option
168
const onlineStatus = trpc.user.onlineStatus.useSubscription(
169
{ userId },
170
{
171
enabled: isOnline,
172
onData: (status) => {
173
console.log("User status changed:", status);
174
},
175
}
176
);
177
178
// Using skip token
179
const notifications = trpc.notifications.subscribe.useSubscription(
180
isOnline ? { userId } : skipToken,
181
{
182
onData: (notification) => {
183
showNotification(notification.message);
184
},
185
}
186
);
187
188
return (
189
<div>
190
<p>Online: {isOnline ? "Yes" : "No"}</p>
191
<p>Subscription status: {onlineStatus.status}</p>
192
</div>
193
);
194
}
195
```
196
197
### Real-time Chat Implementation
198
199
Complete example of a real-time chat using subscriptions.
200
201
```typescript
202
function ChatRoom({ roomId, userId }: { roomId: string; userId: number }) {
203
const [messages, setMessages] = useState<Message[]>([]);
204
const [inputValue, setInputValue] = useState("");
205
206
// Subscribe to new messages
207
const messageSubscription = trpc.chat.messages.useSubscription(
208
{ roomId },
209
{
210
onData: (newMessage) => {
211
setMessages((prev) => [...prev, newMessage]);
212
},
213
onError: (error) => {
214
console.error("Message subscription error:", error);
215
},
216
}
217
);
218
219
// Subscribe to typing indicators
220
const typingSubscription = trpc.chat.typing.useSubscription(
221
{ roomId },
222
{
223
onData: (typingData) => {
224
console.log("Typing:", typingData);
225
},
226
}
227
);
228
229
const sendMessage = trpc.chat.sendMessage.useMutation({
230
onSuccess: () => {
231
setInputValue("");
232
},
233
});
234
235
const handleSendMessage = () => {
236
if (inputValue.trim()) {
237
sendMessage.mutate({
238
roomId,
239
userId,
240
content: inputValue,
241
});
242
}
243
};
244
245
return (
246
<div className="chat-room">
247
<div className="connection-status">
248
Messages: {messageSubscription.status}
249
{messageSubscription.error && (
250
<span>Error: {messageSubscription.error.message}</span>
251
)}
252
</div>
253
254
<div className="messages">
255
{messages.map((message) => (
256
<div key={message.id} className="message">
257
<strong>{message.user.name}:</strong> {message.content}
258
<small>{new Date(message.timestamp).toLocaleTimeString()}</small>
259
</div>
260
))}
261
</div>
262
263
<div className="input-area">
264
<input
265
value={inputValue}
266
onChange={(e) => setInputValue(e.target.value)}
267
onKeyPress={(e) => e.key === "Enter" && handleSendMessage()}
268
placeholder="Type a message..."
269
/>
270
<button
271
onClick={handleSendMessage}
272
disabled={sendMessage.isPending}
273
>
274
Send
275
</button>
276
</div>
277
</div>
278
);
279
}
280
```
281
282
### Live Data Updates
283
284
Use subscriptions to keep displayed data synchronized with real-time changes.
285
286
```typescript
287
function LiveUserList() {
288
const [users, setUsers] = useState<User[]>([]);
289
290
// Initial data fetch
291
const { data: initialUsers } = trpc.users.list.useQuery();
292
293
// Subscribe to user updates
294
const userUpdates = trpc.users.updates.useSubscription(
295
{},
296
{
297
onData: (update) => {
298
setUsers((prevUsers) => {
299
switch (update.type) {
300
case 'user_added':
301
return [...prevUsers, update.user];
302
case 'user_updated':
303
return prevUsers.map((user) =>
304
user.id === update.user.id ? update.user : user
305
);
306
case 'user_removed':
307
return prevUsers.filter((user) => user.id !== update.userId);
308
default:
309
return prevUsers;
310
}
311
});
312
},
313
}
314
);
315
316
// Initialize users when query data is available
317
useEffect(() => {
318
if (initialUsers) {
319
setUsers(initialUsers);
320
}
321
}, [initialUsers]);
322
323
return (
324
<div>
325
<h2>Live User List ({userUpdates.status})</h2>
326
{userUpdates.error && (
327
<div>Subscription error: {userUpdates.error.message}</div>
328
)}
329
<ul>
330
{users.map((user) => (
331
<li key={user.id}>
332
{user.name} - {user.status}
333
</li>
334
))}
335
</ul>
336
</div>
337
);
338
}
339
```
340
341
### Subscription Error Handling
342
343
Implement robust error handling and recovery for subscriptions.
344
345
```typescript
346
function RobustSubscription({ channelId }: { channelId: string }) {
347
const [retryCount, setRetryCount] = useState(0);
348
const maxRetries = 3;
349
350
const subscription = trpc.channel.subscribe.useSubscription(
351
{ channelId },
352
{
353
enabled: retryCount < maxRetries,
354
onData: (data) => {
355
// Reset retry count on successful data reception
356
setRetryCount(0);
357
console.log("Received data:", data);
358
},
359
onError: (error) => {
360
console.error("Subscription error:", error);
361
362
// Implement exponential backoff retry
363
if (retryCount < maxRetries) {
364
setTimeout(() => {
365
setRetryCount((prev) => prev + 1);
366
}, Math.pow(2, retryCount) * 1000);
367
}
368
},
369
onStopped: () => {
370
console.log("Subscription stopped");
371
},
372
}
373
);
374
375
const handleManualRetry = () => {
376
setRetryCount(0);
377
};
378
379
return (
380
<div>
381
<div>Status: {subscription.status}</div>
382
{subscription.error && (
383
<div>
384
<p>Error: {subscription.error.message}</p>
385
{retryCount >= maxRetries ? (
386
<button onClick={handleManualRetry}>
387
Retry Connection
388
</button>
389
) : (
390
<p>Retrying... ({retryCount}/{maxRetries})</p>
391
)}
392
</div>
393
)}
394
{subscription.data && (
395
<div>Latest data: {JSON.stringify(subscription.data)}</div>
396
)}
397
</div>
398
);
399
}
400
```
401
402
## Common Patterns
403
404
### Subscription Cleanup
405
406
Subscriptions are automatically cleaned up when components unmount, but you can also control them manually:
407
408
```typescript
409
function SubscriptionWithCleanup() {
410
const [isSubscribed, setIsSubscribed] = useState(true);
411
412
const subscription = trpc.events.subscribe.useSubscription(
413
{ channel: "global" },
414
{
415
enabled: isSubscribed,
416
}
417
);
418
419
return (
420
<div>
421
<button onClick={() => setIsSubscribed(!isSubscribed)}>
422
{isSubscribed ? "Unsubscribe" : "Subscribe"}
423
</button>
424
<p>Status: {subscription.status}</p>
425
</div>
426
);
427
}
428
```
429
430
### Multiple Subscriptions
431
432
Handle multiple related subscriptions in a single component:
433
434
```typescript
435
function MultipleSubscriptions({ userId }: { userId: number }) {
436
const notifications = trpc.notifications.subscribe.useSubscription(
437
{ userId },
438
{ onData: (data) => console.log("Notification:", data) }
439
);
440
441
const messages = trpc.messages.subscribe.useSubscription(
442
{ userId },
443
{ onData: (data) => console.log("Message:", data) }
444
);
445
446
const presence = trpc.presence.subscribe.useSubscription(
447
{ userId },
448
{ onData: (data) => console.log("Presence:", data) }
449
);
450
451
const allConnected = [notifications, messages, presence].every(
452
(sub) => sub.status === 'connected'
453
);
454
455
return (
456
<div>
457
<p>All subscriptions connected: {allConnected ? "Yes" : "No"}</p>
458
</div>
459
);
460
}
461
```
462
463
### Subscription with Authentication
464
465
Handle authentication in subscription connections:
466
467
```typescript
468
function AuthenticatedSubscription({ token }: { token: string }) {
469
const subscription = trpc.private.updates.useSubscription(
470
{ channel: "user-updates" },
471
{
472
trpc: {
473
headers: {
474
Authorization: `Bearer ${token}`,
475
},
476
},
477
onError: (error) => {
478
if (error.data?.code === "UNAUTHORIZED") {
479
// Handle authentication error
480
redirectToLogin();
481
}
482
},
483
}
484
);
485
486
return (
487
<div>
488
{subscription.data && <div>Update: {subscription.data.message}</div>}
489
</div>
490
);
491
}
492
```