Skip to content

Commit 3b96401

Browse files
committed
Update README for throwOnTag rename and signal support
1 parent 950ea99 commit 3b96401

1 file changed

Lines changed: 57 additions & 21 deletions

File tree

README.md

Lines changed: 57 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ Uses TanStack Start's `createIsomorphicFn` to pick the correct runtime at compil
161161

162162
```ts
163163
// src/runtimes/get-runtime.ts
164+
import { notFound } from "@tanstack/react-router";
164165
import { createIsomorphicFn } from "@tanstack/react-start";
165166
import { makeCallApiPromise } from "effect-tanstack-start/client";
166167
import { ApiClient } from "@/services/api-client-tag";
@@ -171,7 +172,11 @@ export const getRuntime = createIsomorphicFn()
171172
.server(() => serverRuntime)
172173
.client(() => clientRuntime);
173174

174-
export const callApiPromise = makeCallApiPromise(ApiClient, getRuntime);
175+
export const callApiPromise = makeCallApiPromise(ApiClient, getRuntime, {
176+
throwOnTag: {
177+
TodoNotFound: () => notFound(),
178+
},
179+
});
175180
```
176181

177182
### 7. API splat route
@@ -208,12 +213,15 @@ import { createFileRoute } from "@tanstack/react-router";
208213
import { callApiPromise } from "@/runtimes/get-runtime";
209214

210215
export const Route = createFileRoute("/")({
211-
loader: () => callApiPromise((api) => api.todos.list()),
216+
loader: ({ abortController }) =>
217+
callApiPromise((api) => api.todos.list(), {
218+
signal: abortController.signal,
219+
}),
212220
component: Todos,
213221
});
214222
```
215223

216-
This works identically on server (SSR) and client (navigation). At SSR time, the handler is called directly. In the browser, it makes an HTTP request to `/api/todos`.
224+
This works identically on server (SSR) and client (navigation). At SSR time, the handler is called directly. In the browser, it makes an HTTP request to `/api/todos`. Passing `signal` ensures in-flight API calls are cancelled when the user navigates away.
217225

218226
### Components
219227

@@ -235,32 +243,35 @@ The callback passed to `callApiPromise` returns an `Effect`, so you can compose
235243
import { Effect } from "effect";
236244

237245
// Combine multiple API calls
238-
loader: () =>
239-
callApiPromise((api) =>
240-
Effect.all({
241-
todos: api.todos.list(),
242-
stats: api.dashboard.stats(),
243-
}),
246+
loader: ({ abortController }) =>
247+
callApiPromise(
248+
(api) =>
249+
Effect.all({
250+
todos: api.todos.list(),
251+
stats: api.dashboard.stats(),
252+
}),
253+
{ signal: abortController.signal },
244254
),
245255
```
246256

247257
### Protected routes
248258

249-
Use a layout route with `beforeLoad` to check auth before loading child routes:
259+
Use a layout route with `beforeLoad` to check auth before loading child routes. `throwOnTag` maps Effect error tags to thrown values — TanStack Router intercepts these to trigger redirects, `notFound()`, etc.
250260

251261
```ts
252262
// src/routes/_authed.tsx
253263
import { createFileRoute, Outlet, redirect } from "@tanstack/react-router";
254264
import { callApiPromise } from "@/runtimes/get-runtime";
255265

256266
export const Route = createFileRoute("/_authed")({
257-
beforeLoad: async ({ location }) => {
258-
try {
259-
const session = await callApiPromise((api) => api.auth.me());
260-
return { user: session };
261-
} catch {
262-
throw redirect({ to: "/login", search: { redirect: location.href } });
263-
}
267+
beforeLoad: async ({ location, abortController }) => {
268+
const session = await callApiPromise((api) => api.auth.me(), {
269+
throwOnTag: {
270+
Unauthorized: () => redirect({ to: "/login", search: { redirect: location.href } }),
271+
},
272+
signal: abortController.signal,
273+
});
274+
return { user: session };
264275
},
265276
component: () => <Outlet />,
266277
});
@@ -286,15 +297,40 @@ Creates a `Context.Tag` typed from your `HttpApi` contract. The tag's service ty
286297

287298
Both `makeSsrApiClientLayer` and `makeHttpApiClientLayer` provide this tag with different implementations.
288299

289-
#### `makeCallApiPromise(clientTag, getRuntime)`
300+
#### `makeCallApiPromise(clientTag, getRuntime, options?)`
290301

291302
Creates a convenience function that resolves the `ApiClient` from the appropriate runtime and runs the effect as a `Promise`. Designed for use in route loaders and event handlers.
292303

304+
Factory-level options:
305+
306+
| Option | Type | Description |
307+
| ------------ | ---------------------------- | ----------------------------------------------------------------------------------------------------------- |
308+
| `throwOnTag` | `Record<string, (e) => any>` | Global error mappings — keys are error `_tag` strings, handlers return a value to throw (e.g. `notFound()`) |
309+
310+
Per-call options:
311+
312+
| Option | Type | Description |
313+
| ------------ | ---------------------------- | ------------------------------------------------------------ |
314+
| `throwOnTag` | `Record<string, (e) => any>` | Per-call error mappings — override or extend global mappings |
315+
| `signal` | `AbortSignal` | Abort signal to cancel the underlying Effect fiber |
316+
293317
```ts
294-
const callApiPromise = makeCallApiPromise(ApiClient, getRuntime);
318+
const callApiPromise = makeCallApiPromise(ApiClient, getRuntime, {
319+
throwOnTag: {
320+
TodoNotFound: () => notFound(),
321+
},
322+
});
323+
324+
// In a loader — pass signal for cancellation on navigation
325+
loader: ({ abortController }) =>
326+
callApiPromise((api) => api.todos.list(), {
327+
signal: abortController.signal,
328+
});
295329

296-
// Usage — one-liner in a loader
297-
loader: () => callApiPromise((api) => api.todos.list());
330+
// Per-call throwOnTag for context-dependent mappings
331+
const session = await callApiPromise((api) => api.auth.me(), {
332+
throwOnTag: { Unauthorized: () => redirect({ to: "/login" }) },
333+
});
298334
```
299335

300336
#### `makeHttpApiClientLayer(api, clientTag, options?)`

0 commit comments

Comments
 (0)