or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

hook-creation.mdindex.mdmutation-hooks.mdquery-hooks.mdquery-keys.mdquery-utilities.mdreact-server-components.mdserver-side-helpers.mdsubscription-hooks.md

subscription-hooks.mddocs/

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

```