react-router-effect 2.1.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # react-router-effect
2
2
 
3
3
  Serve a [React Router](https://reactrouter.com) framework-mode app from [Effect](https://effect.website)'s
4
- Node HTTP server, and write its loaders, actions and middleware as Effects.
4
+ HTTP router, on Node or on Cloudflare Workers. Write its loaders, actions and middleware as Effects.
5
5
 
6
6
  A route function that returns an `Effect` runs with your app's services. When it fails with a tagged
7
7
  error, the route factory you configure once decides what happens: recover with data for
@@ -24,17 +24,21 @@ If your app does not serve React Router from an Effect HTTP server, read
24
24
 
25
25
  ## Install
26
26
 
27
+ On Node:
28
+
27
29
  ```bash
28
- vp add react-router-effect effect @effect/platform-node
30
+ vp add react-router-effect effect react-router @effect/platform-node
29
31
  ```
30
32
 
31
- `effect`, `@effect/platform-node` and `react-router` are peer dependencies. The library requires
32
- Effect 4.1 (`effect@^4.1.0`). Only `react-router-effect/vite` imports `@effect/platform-node`.
33
+ `effect`, `react-router` and `@effect/platform-node` are peer dependencies. The library requires
34
+ Effect 4.1 (`effect@^4.1.0`) and Node 22 or later. Only `react-router-effect/vite` imports
35
+ `@effect/platform-node`, so another platform does not need it. The README of each example lists the
36
+ packages of its platform. Read [Platforms](#platforms).
33
37
 
34
38
  ## Get started
35
39
 
36
- [`examples/basic`](examples/basic) is an app with these files. It also has development with
37
- Vite, tests against the production build, and benchmarks.
40
+ These steps serve an app on Node in production. [`examples/node`](examples/node) is an app with
41
+ these files. It also has development with Vite, tests against the production build, and benchmarks.
38
42
 
39
43
  ### 1. Create the route factory
40
44
 
@@ -65,51 +69,59 @@ export const { Respond } = routeFactory;
65
69
 
66
70
  ### 2. Create the server app
67
71
 
68
- `ReactRouterHttp.make` returns an Effect that makes an Effect HTTP app. The app runs React Router
69
- with your route factory.
72
+ `App` is a layer of routes on Effect's `HttpRouter`. `ReactRouter.layer` loads the build with your
73
+ route factory. `ReactRouter.routes` registers React Router as the catch-all route.
74
+ `ReactRouterAssets.layer` registers the files of the client build. Your own routes and middleware go
75
+ in the same `Layer.mergeAll`.
70
76
 
71
77
  ```ts
72
78
  // server/app.ts
73
- import { ReactRouterHttp } from "react-router-effect/server";
79
+ import { Layer } from "effect";
80
+ import { ReactRouter, ReactRouterAssets } from "react-router-effect/server";
74
81
 
75
82
  import { routeFactory } from "../app/lib/route-factory.server";
76
83
 
77
- export const reactRouterApp = ReactRouterHttp.make({
78
- build: () => import("virtual:react-router/server-build"),
79
- factory: routeFactory,
80
- clientAssets: true, // serves build/client in production
81
- });
84
+ export const App = Layer.mergeAll(ReactRouter.routes, ReactRouterAssets.layer).pipe(
85
+ Layer.provide(
86
+ ReactRouter.layer({
87
+ build: () => import("virtual:react-router/server-build"),
88
+ factory: routeFactory,
89
+ mode: import.meta.env.MODE,
90
+ }),
91
+ ),
92
+ );
82
93
  ```
83
94
 
84
95
  ### 3. Serve it
85
96
 
86
- The server makes the app once, when it starts, and serves it with its own Effect HTTP server and its
87
- own middleware. In production, making the app loads the build and reads the client build. If the
88
- server cannot read the client build, it fails to start, and the error names the resolved directory.
97
+ `server.js` serves `App` with `HttpRouter.serve`, which also logs each request. When the server
98
+ starts, `App` loads the build and reads the client build. If the server cannot read the client
99
+ build, it fails to start with a `ClientBuildError` that names the resolved directory.
89
100
 
90
- ```ts
91
- // server/main.ts
101
+ ```js
102
+ // server.js
92
103
  import { createServer } from "node:http";
93
104
 
94
- import { NodeHttpServer, NodeRuntime } from "@effect/platform-node";
95
- import { Effect, Layer } from "effect";
96
- import { HttpMiddleware, HttpServer } from "effect/http";
105
+ import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer";
106
+ import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
107
+ import { Layer } from "effect";
108
+ import { HttpRouter } from "effect/http";
97
109
 
98
- import { reactRouterApp } from "./app";
110
+ import { App } from "./build/server/index.js";
99
111
 
100
- Layer.unwrap(
101
- Effect.map(reactRouterApp, (app) => HttpServer.serve(app, HttpMiddleware.logger)),
102
- ).pipe(
103
- Layer.provide(NodeHttpServer.layer(() => createServer(), { port: 3000 })),
104
- Layer.launch,
105
- NodeRuntime.runMain,
112
+ NodeRuntime.runMain(
113
+ Layer.launch(
114
+ HttpRouter.serve(App).pipe(
115
+ Layer.provide(NodeHttpServer.layer(() => createServer(), { port: 3000 })),
116
+ ),
117
+ ),
106
118
  );
107
119
  ```
108
120
 
109
121
  ### 4. Build the server
110
122
 
111
- Make `server/main.ts` the entry of the server build. The build then contains your server, and
112
- `react-router build` writes it to `build/server/index.js`.
123
+ Make `server/app.ts` the entry of the server build. `react-router build` then writes `App` to
124
+ `build/server/index.js`.
113
125
 
114
126
  ```ts
115
127
  // vite.config.ts
@@ -118,16 +130,17 @@ import { defineConfig } from "vite";
118
130
 
119
131
  export default defineConfig({
120
132
  plugins: [reactRouter()],
121
- environments: { ssr: { build: { rollupOptions: { input: "./server/main.ts" } } } },
133
+ environments: { ssr: { build: { rollupOptions: { input: "./server/app.ts" } } } },
122
134
  });
123
135
  ```
124
136
 
125
137
  ```bash
126
138
  react-router build
127
- NODE_ENV=production node build/server/index.js
139
+ NODE_ENV=production node server.js # React's server build reads NODE_ENV
128
140
  ```
129
141
 
130
- For development, read [Development with Vite](#development-with-vite).
142
+ For development with Vite, the Node example's `server.js` serves the app through
143
+ `ReactRouterVite.layer`. Read [Development with Vite](examples/node/README.md#development-with-vite).
131
144
 
132
145
  ### 5. Write routes as Effects
133
146
 
@@ -164,6 +177,28 @@ middleware](#plain-effect-loaders-actions-and-middleware) explains why. A route
164
177
  not return an Effect passes through to React Router unchanged, so promise loaders keep working in
165
178
  the same app.
166
179
 
180
+ ## Platforms
181
+
182
+ `App` is the same layer on every platform. Only the file that serves it changes. Effect has two
183
+ functions that serve it:
184
+
185
+ - `HttpRouter.serve(App)` with a server layer, such as `NodeHttpServer.layer` on Node.
186
+ - `HttpRouter.toWebHandler(App)`, which returns a fetch handler, such as a Cloudflare Worker's.
187
+
188
+ | Platform | Example | Serves `App` with |
189
+ | ------------------ | -------------------------------------------- | --------------------------------------------- |
190
+ | Node | [`examples/node`](examples/node) | `HttpRouter.serve` and `NodeHttpServer.layer` |
191
+ | Cloudflare Workers | [`examples/cloudflare`](examples/cloudflare) | `HttpRouter.toWebHandler` |
192
+
193
+ The README of each example has the details of its platform. These rules apply on every platform:
194
+
195
+ - App-wide services go in the route factory's `runtime`. Read
196
+ [App-wide services with a runtime](#app-wide-services-with-a-runtime).
197
+ - Per-request services go in a router middleware. Read [Per-request services](#per-request-services).
198
+ - A platform service, such as a tracer, goes where the serve function reads it. `HttpRouter.serve`
199
+ reads the services that `Layer.provide` gives to its layer. `HttpRouter.toWebHandler` reads the
200
+ services that `Layer.provideMerge` adds to the layer that it gets.
201
+
167
202
  ## Handle errors
168
203
 
169
204
  An Effect can short-circuit in three ways, with the `Respond` helpers:
@@ -224,6 +259,79 @@ service-specific error that is not a declared domain error. Catch it or map it.
224
259
  defaults for declared errors. The errors of one feature or service need explicit handling.
225
260
  [The route check](#the-route-check) reports an unhandled error at compile time.
226
261
 
262
+ ## Defer data
263
+
264
+ The route factory returns `defer`. It starts an Effect without waiting for it, and gives a promise
265
+ of the result. Put the promise in the loader data. React Router streams it to `<Await>` after the
266
+ first response. `defer` is unstable: a minor version can change it.
267
+
268
+ ```tsx
269
+ // app/lib/route-factory.server.ts
270
+ export const { Respond, defer } = routeFactory;
271
+
272
+ // app/routes/product.tsx
273
+ export const loader = Effect.fn("loader.product")(function* ({
274
+ params,
275
+ }: EffectRouteArgs<Route.LoaderArgs>) {
276
+ const product = yield* (yield* Products).get(params.id);
277
+ return {
278
+ product,
279
+ reviews: yield* defer((yield* Reviews).list(params.id)),
280
+ stats: yield* (yield* Stats).load(params.id).pipe(
281
+ Effect.catchTag("StatsUnavailable", () => Effect.succeed(null)),
282
+ defer,
283
+ ),
284
+ };
285
+ });
286
+
287
+ export default function Product({ loaderData }: Route.ComponentProps) {
288
+ return (
289
+ <Suspense fallback={<p>Loading reviews</p>}>
290
+ <Await resolve={loaderData.reviews} errorElement={<ReviewsError />}>
291
+ {(reviews) => <ReviewList reviews={reviews} />}
292
+ </Await>
293
+ </Suspense>
294
+ );
295
+ }
296
+ ```
297
+
298
+ The loader yields `defer`, and the deferred work starts at that point. The work and the rest of the
299
+ loader run at the same time. The work gets the services and the span of the loader, so its spans are in the trace of the request. It
300
+ keeps running after the loader returns. The work stops in two cases:
301
+
302
+ - The request aborts, for example because the client disconnects.
303
+ - The loader fails after it deferred the work. The response then has no data that waits for it.
304
+
305
+ A client-side navigation to another page does not abort the request. React Router aborts the request
306
+ of a navigation only while its loaders run. The work then runs until it ends, or until React
307
+ Router's `streamTimeout` ends the stream.
308
+
309
+ The response starts before the deferred work fails, so a failure cannot redirect or change the
310
+ response. The error handlers of the factory turn a failure into a value or a rejection:
311
+
312
+ | Failure of the deferred Effect | The promise |
313
+ | --------------------------------------------------------------- | ------------------------------------------------- |
314
+ | A handler recovers it, with `Respond.early` or `Effect.succeed` | resolves with the value → `<Await>` |
315
+ | `Respond.early(value)` raised directly | resolves with `value` → `<Await>` |
316
+ | A handler throws it, with `Respond.throw` or `Effect.fail` | rejects with an `ErrorResponse` → `useAsyncError` |
317
+ | `Respond.throw(value, init)` raised directly | rejects with an `ErrorResponse` → `useAsyncError` |
318
+ | A handler redirects, or a domain error has no handler | rejects with a plain `Error` |
319
+ | A defect or an interrupt | rejects with a plain `Error` |
320
+
321
+ The plain `Error` has the message `deferredFailureMessage` (`"Deferred route data failed"`) and no
322
+ stack, in every server mode. The server logs the full cause of a defect. A recovered
323
+ `Respond.early(value, init)` gives `value` without `init`, because a promise has no response.
324
+
325
+ The deferred Effect can fail only with the factory's domain errors, `Respond.early` and
326
+ `Respond.throw`. Any other error does not compile, and the message names the error tags. Handle
327
+ those errors before `defer`, or make them defects with `Effect.orDie`. In a factory without domain
328
+ errors, the deferred Effect must fail with `never`.
329
+
330
+ The promise type includes what the handlers recover, so `Route.ComponentProps` types the value that
331
+ `<Await>` renders. The type is `DeferredPromise<A>`, a subtype of `Promise<A>`. The
332
+ `promiseInEffectSuccess` rule of the Effect language service reports only the global `Promise`
333
+ type, so it does not report a `yield* defer(...)`.
334
+
227
335
  ## Provide services
228
336
 
229
337
  ### App-wide services with a runtime
@@ -255,19 +363,37 @@ makeEffectRouteFactory()(() => ({}));
255
363
  The runtime provides app-wide services. A request-scoped service is the current user, a request id,
256
364
  or a transaction for one request. There are two ways to provide one.
257
365
 
258
- The adapter option `requestContext` builds the services before React Router runs. Every middleware,
259
- loader and action sees them, and no middleware has to set them:
366
+ A route-scoped router middleware on `ReactRouter.routes` provides services to React Router's
367
+ requests. Every middleware, loader and action sees them, and no React Router middleware has to set
368
+ them:
260
369
 
261
370
  ```ts
262
- ReactRouterHttp.make({
263
- build: () => import("virtual:react-router/server-build"),
264
- factory: routeFactory,
265
- requestContext: (request) =>
266
- Context.make(RequestInfo, {
267
- id: request.headers.get("x-request-id") ?? crypto.randomUUID(),
268
- startedAt: Date.now(),
371
+ // server/app.ts
372
+ const RequestServices = HttpRouter.middleware((app) =>
373
+ Effect.provideServiceEffect(
374
+ app,
375
+ RequestInfo,
376
+ HttpServerRequest.HttpServerRequest.use((request) =>
377
+ Effect.succeed({
378
+ id: request.headers["x-request-id"] ?? crypto.randomUUID(),
379
+ startedAt: Date.now(),
380
+ }),
381
+ ),
382
+ ),
383
+ ).layer;
384
+
385
+ export const App = Layer.mergeAll(
386
+ ReactRouter.routes.pipe(Layer.provide(RequestServices)),
387
+ ReactRouterAssets.layer,
388
+ ).pipe(
389
+ Layer.provide(
390
+ ReactRouter.layer({
391
+ build: () => import("virtual:react-router/server-build"),
392
+ factory: routeFactory,
393
+ mode: import.meta.env.MODE,
269
394
  }),
270
- });
395
+ ),
396
+ );
271
397
  ```
272
398
 
273
399
  A [middleware](https://reactrouter.com/how-to/middleware) can also provide services. An effect
@@ -308,14 +434,26 @@ A route Effect can then require the runtime's services and the request's service
308
434
  requirement is a compile error. `RequestContextKey<ReqServices>` is an alias of
309
435
  `RouterContext<Context.Context<ReqServices>>`.
310
436
 
437
+ The adapter sets this key to the request fiber's context before React Router runs. That context
438
+ holds the server span, `HttpServerRequest` and the services of the router middleware. Do not set the
439
+ key to a new context in a React Router middleware, as an app without the adapter does. A new context
440
+ replaces those services for every function after that middleware. Provide request services with a
441
+ router middleware or an effect middleware, as this section shows.
442
+
311
443
  ## The adapter
312
444
 
313
- `react-router-effect/server` is a React Router adapter for Effect's Node HTTP server
314
- (`@effect/platform-node`). It handles everything between an `HttpServerRequest` and React Router. That
315
- is the Web `Request` and its abort signal, the route factory, and the client build. It also converts
316
- React Router's `Response`, with its streamed body and its cookies. The adapter is an
317
- Effect HTTP app like any other. The app serves it with its own Effect HTTP server and keeps its own
318
- middleware: logging, compression, tracing, proxies and other routes.
445
+ `react-router-effect/server` is a React Router adapter for Effect's HTTP router, on every platform
446
+ that the router runs on. It is the Effect counterpart of `@react-router/express`. It
447
+ handles everything between an `HttpServerRequest` and React Router. That is the Web `Request` and
448
+ its abort signal, the route factory, and the client build. It also converts React Router's
449
+ `Response`, with its streamed body and its cookies. The app keeps its own server, middleware and
450
+ routes.
451
+
452
+ The adapter's API is unstable, and a minor version can change it. Every export of
453
+ `react-router-effect/server` and `react-router-effect/vite` has the JSDoc tag `@stability unstable`.
454
+ The route factory and the other exports of `react-router-effect` are stable, except the route
455
+ metadata. The `routeMeta` configuration, the `Register` key `routeMeta`, `RouteMetaArgs`,
456
+ `WithRouteMeta` and `routeMetaKey` are unstable, and a minor version can change them.
319
457
 
320
458
  React Router keeps doing what it does for any other server adapter: documents, `.data` requests,
321
459
  streamed promises, resource routes, the lazy route manifest and middleware. Before React Router
@@ -325,66 +463,165 @@ sees the build, the adapter runs every loader, action and middleware through you
325
463
  Its failures go through the factory's error handlers.
326
464
  - Any other route function passes through. Promise loaders and `makeLoader` wrappers keep working.
327
465
 
328
- The options:
329
-
330
- - `build` is the server build, or a function that loads it.
331
- - `factory` is the route factory.
332
- - `requestContext: (request) => Context` builds the request's services before React Router runs.
333
- - `clientAssets: true` serves the client build, `build.assetsBuildDirectory` under
334
- `build.publicPath`, before React Router. Files under `assets/` use a content hash in their name,
335
- and the adapter caches them for a year as immutable. It caches other files for an hour. It serves
336
- the files as `express.static` does. A directory serves its index file, such as a prerendered page.
337
- A directory path without the trailing slash redirects to the path with it. It does not serve
338
- dotfiles, and a request it does not serve goes to React Router. Like `express.static`, it looks up
339
- each GET and HEAD request in the file system. It serves only a build that it loads once: in
340
- production mode, or a build value. It never serves a development build, where Vite serves the
341
- client. `assetsBuildDirectory` is relative to the working directory of the server. If the adapter
342
- cannot read it, making the app fails with a `ClientBuildError` that names the resolved directory.
343
- It needs the platform's file system, which `NodeHttpServer.layer` provides.
344
- - `mode` is React Router's server mode. The default is `process.env.NODE_ENV`. In production the
345
- adapter loads the build once.
346
-
347
- The Web request comes from `HttpServerRequest.toWeb`, body included. Effect's stream sends the
348
- `100 Continue` when React Router reads the body of a request that sends `Expect: 100-continue`.
349
-
350
- The request URL is the one Effect derives from the request. An app behind a proxy applies the
351
- headers it trusts in its own middleware, with `request.modify` or `HttpMiddleware.xForwardedHeaders`.
352
- The request signal aborts only if the response ends early. That happens when the client leaves while
353
- React Router runs, or while the body streams. A completed response never aborts it.
466
+ The layers:
467
+
468
+ - `ReactRouter.layer(options)` provides the `ReactRouter` service. Its options are these:
469
+ - `build` is the server build, or a function that loads it.
470
+ - `factory` is the route factory.
471
+ - `mode` is React Router's server mode, and you must give it. In a Vite app, pass
472
+ `import.meta.env.MODE`. React Router runs in production mode for any mode but `development`
473
+ and `test`, and so does the layer. At layer build, the layer loads a build value once, or a
474
+ build function in production mode. In `development` and `test`, a build function loads for
475
+ each request.
476
+ - `loadContext` makes React Router's load context for each request. Read
477
+ [The load context](#the-load-context).
478
+ - `ReactRouter.routes` registers React Router as the catch-all route on `*`.
479
+ - `ReactRouterAssets.layer` registers the files of the client build. Read
480
+ [The client build](#the-client-build).
481
+ - `ReactRouterVite.layer`, from `react-router-effect/vite`, serves the app in development on Node.
482
+ Read [Development with Vite](examples/node/README.md#development-with-vite).
483
+
484
+ Provide one value of `ReactRouter.layer` to every layer that reads the service, as `App` does with
485
+ `Layer.provide`. Two values load the build twice.
486
+
487
+ ### The load context
488
+
489
+ `loadContext` does for the adapter what `getLoadContext` does for `@react-router/express`. It
490
+ returns an Effect of a `RouterContextProvider`. Plain loaders and actions, and `handleRequest` in
491
+ `entry.server.tsx`, read its values with `context.get`. Route Effects read services from their
492
+ Effect context. So give route Effects services with router middleware, not with `loadContext`.
354
493
 
355
- React Router logs and answers the errors of route code. If the adapter's own steps fail, for example
356
- a `requestContext` that throws, the adapter logs the error with `Effect.logError` and answers 500.
357
- The log goes to the loggers of the app's Effect runtime.
494
+ ```ts
495
+ ReactRouter.layer({
496
+ build: () => import("virtual:react-router/server-build"),
497
+ factory: routeFactory,
498
+ mode: import.meta.env.MODE,
499
+ loadContext: (request) =>
500
+ Effect.sync(() => {
501
+ const context = new RouterContextProvider();
502
+ context.set(localeKey, request.headers["accept-language"] ?? "en");
503
+ return context;
504
+ }),
505
+ });
506
+ ```
358
507
 
359
- ### Development with Vite
508
+ The Effect gets the services of the layer and the services of the request. The services of the
509
+ request, such as its scope, win. The adapter adds its own keys to the provider. If the Effect dies,
510
+ the client gets an empty 500.
360
511
 
361
- In development Vite serves the client. `react-router-effect/vite` runs Vite's middleware in front of
362
- the app, on Effect's Node HTTP server:
512
+ ### Routes and middleware
363
513
 
364
- ```ts
365
- import { ReactRouterVite } from "react-router-effect/vite";
514
+ The route that a request matches decides which middleware runs on it. Effect has three kinds of
515
+ middleware:
366
516
 
367
- const vite = await createServer({ server: { middlewareMode: true } });
368
- HttpServer.serve(Effect.flatten(reactRouterApp).pipe(ReactRouterVite.devMiddleware(vite)));
517
+ - A server middleware, the `middleware` option of `HttpRouter.serve`, wraps every request. It also
518
+ wraps the send of the response, so a change to the response with `Effect.map` does not reach the
519
+ client. `HttpMiddleware.compression` works there.
520
+ - A global router middleware, `HttpRouter.middleware(fn, { global: true })`, wraps every route.
521
+ - A route-scoped middleware, `HttpRouter.middleware(fn).layer`, applies only to the routes of the
522
+ layers that you provide it to.
523
+
524
+ Route-scoped middleware selects the middleware of each kind of request. `HttpRouter.serve` logs
525
+ every request. To leave file requests out of that log, provide `HttpRouter.disableLogger` to the
526
+ assets layer only:
527
+
528
+ ```ts
529
+ export const App = Layer.mergeAll(
530
+ ReactRouter.routes.pipe(Layer.provide(RequestServices)),
531
+ ReactRouterAssets.layer.pipe(Layer.provide(HttpRouter.disableLogger)),
532
+ HttpRouter.middleware(HttpMiddleware.compression(), { global: true }),
533
+ ).pipe(
534
+ Layer.provide(
535
+ ReactRouter.layer({
536
+ build: () => import("virtual:react-router/server-build"),
537
+ factory: routeFactory,
538
+ mode: import.meta.env.MODE,
539
+ }),
540
+ ),
541
+ );
369
542
  ```
370
543
 
371
- In development, making the app does nothing but return it, so the server can make it for each
372
- request. Load the module that calls `ReactRouterHttp.make` through `vite.ssrLoadModule`. Then the route
373
- modules, the factory and the adapter share Vite's module graph. The example's `server.js` and
374
- `server/main.ts` do this.
544
+ The order of two route-scoped middleware layers on one route is not fixed. If the order matters,
545
+ join them into one middleware with `combine`:
546
+ `HttpRouter.middleware(first).combine(HttpRouter.middleware(second)).layer`.
547
+
548
+ These rules apply to the routes:
549
+
550
+ - An exact route wins over a route with a parameter, and both win over a wildcard. So your own
551
+ routes and the files of the client build win over React Router's `*`.
552
+ - A route of your own on `*` or `/*`, for a method that React Router's route has, collides with it.
553
+ Then the server fails to start. A route of your own with the path of a file of the client build
554
+ fails the same way.
555
+ - If a HEAD request matches no HEAD route, it uses the GET route of its path. `ReactRouter.routes`
556
+ registers no HEAD route. So a GET route of your own answers HEAD, as it does in Express.
557
+ - React Router's route takes the methods of `HttpMethod.all`: GET, POST, PUT, DELETE, PATCH,
558
+ OPTIONS, TRACE and QUERY, and HEAD through GET. A request with another method, such as PROPFIND,
559
+ gets a 404 from the router.
560
+ - Register `ReactRouter.routes` at the root of the router. Under a prefix, React Router does not
561
+ get the path that it expects. To serve the app under a path, set React Router's `basename`.
562
+ - The router ignores the case of the path. On a file system that is case-sensitive, a request for a
563
+ file with the wrong case matches the file's route and gets a 404.
564
+
565
+ ### The client build
566
+
567
+ `ReactRouterAssets.layer` serves `build.assetsBuildDirectory` under `build.publicPath`, as the two
568
+ `express.static` lines of React Router's Express template do. It needs the platform's file system,
569
+ which `NodeHttpServer.layer` provides. A Cloudflare Worker serves the client build from its static
570
+ assets, and does not use this layer.
571
+
572
+ - The hashed directory is the directory of the build's manifest URL, such as `/assets`. React
573
+ Router's build writes files with a content hash in their names there. The layer caches them for a
574
+ year as immutable. A request under the hashed directory gets a 404 for a file that the directory
575
+ does not have. A path with `..` does not get out of the hashed directory.
576
+ - Each other file gets its own route for GET and HEAD, and the layer caches it for an hour. At
577
+ server start, the layer reads the client build. If a file is added outside the hashed directory
578
+ later, the layer serves it only after a restart.
579
+ - A directory with an `index.html`, such as a prerendered page, serves that file. Its path without
580
+ the trailing slash redirects to the path with it. A root `index.html`, such as a prerendered home
581
+ page, serves `/`, and React Router's catch-all yields to it.
582
+ - If the manifest URL is not under `publicPath`, or it is at the root of `publicPath`, there is no
583
+ hashed directory. Then the layer caches every file for an hour.
584
+ - The layer serves no dotfiles, such as `.vite/manifest.json`, as `express.static` does.
585
+ - The layer registers nothing for a build that loads for each request, such as a development build.
586
+ Vite serves the client there. It also registers nothing for a `publicPath` on another origin.
587
+ - `assetsBuildDirectory` is relative to the working directory of the server. If the layer cannot
588
+ read it, the server fails to start with a `ClientBuildError` that names the resolved directory.
589
+ If a file name has a `*`, the server fails to start with a `ClientBuildError` too, because the
590
+ router cannot match that name.
591
+
592
+ ### Requests
593
+
594
+ The Web request comes from `HttpServerRequest.toWeb`, body included. On Effect's Node server, the
595
+ request has the changes that middleware makes to the `HttpServerRequest`, such as the headers of
596
+ `HttpMiddleware.xForwardedHeaders`. On a Web server, such as a Cloudflare Worker, React Router gets
597
+ the runtime's own request. The README of each example describes the requests of its platform.
598
+
599
+ If the client leaves while React Router runs or while the body streams, the response ends early, and
600
+ the request signal aborts. A completed response never aborts it. On Cloudflare Workers, the abort
601
+ comes from Cloudflare's own request signal. For the conditions of that abort, read the
602
+ [Cloudflare example](examples/cloudflare/README.md#requests).
603
+
604
+ React Router logs and answers the errors of route code. If the adapter's own steps fail, for example
605
+ a `loadContext` that dies, the adapter logs the error with `Effect.logError`. Then it answers an
606
+ empty 500. The log goes to the loggers of the app's Effect runtime.
375
607
 
376
608
  ### Cookies
377
609
 
378
610
  Set cookies from route code with React Router's cookie helpers, `createCookie` and the session
379
611
  storages, as in any React Router app. The adapter converts React Router's `Set-Cookie` values into
380
- the response's `cookies`, as `HttpServerResponse.fromWeb` does. Effect HTTP middleware around the
381
- adapter can read and change them like any other cookie. A response that replaces React Router's has
382
- none of them.
612
+ the response's `cookies`, as `HttpServerResponse.fromWeb` does. A router middleware on
613
+ `ReactRouter.routes` can read and change them like any other cookie. A response that replaces React
614
+ Router's has none of them.
383
615
 
384
616
  Effect keeps one cookie per name and only the attributes it knows: `Max-Age`, `Domain`, `Path`,
385
- `Expires`, `HttpOnly`, `Secure`, `Partitioned`, `Priority` and `SameSite`. If one response sets the
386
- same cookie name twice, for example to expire it on one path and set it on another, only the last
387
- one is sent.
617
+ `Expires`, `HttpOnly`, `Secure`, `Partitioned`, `Priority` and `SameSite`. One response can set the
618
+ same cookie name twice, for example to expire it on one path and set it on another. Then Effect
619
+ sends only the last one.
620
+
621
+ Effect accepts only cookie names that are RFC 6265 tokens: letters, digits and
622
+ ``!#$%&'*+-.^_`|~``. React Router's `createCookie` and session storages also accept other names,
623
+ such as `app/session` or `user:prefs`. The adapter does not send a cookie with such a name. It logs
624
+ an error with the cookie's name instead. Give every cookie a token name.
388
625
 
389
626
  ### Content type
390
627
 
@@ -401,10 +638,15 @@ return new Response(Readable.toWeb(file) as ReadableStream, {
401
638
 
402
639
  ### Tracing
403
640
 
404
- When the request has a server span, the adapter traces React Router. The span comes from
405
- `HttpMiddleware.tracer` with a tracer that exports spans, such as `OtelTracer`. Each middleware,
406
- loader and action runs in a span of its own, such as `react-router.loader routes/todos`. The spans
407
- nest as React Router calls the functions. This is the example's `GET /todos`:
641
+ When the request has a server span, the adapter traces React Router. If the app provides a tracer
642
+ that exports spans, such as `OtelTracer`, Effect's HTTP server makes the server span. Give the
643
+ tracer where the serve function reads it, as [Platforms](#platforms) tells. Effect serves each
644
+ request with the services of that layer. A tracer that the serve function does not read makes no
645
+ server span, so the adapter adds no spans. The README of each example shows where its tracer goes.
646
+
647
+ Each middleware, loader and action runs in a span of its own, such as
648
+ `react-router.loader routes/todos`.
649
+ The spans nest as React Router calls the functions. This is the example's `GET /todos`:
408
650
 
409
651
  ```text
410
652
  GET the server span, with http.route=/todos
@@ -423,6 +665,12 @@ instrumentation traces nest there too, such as a database driver that a promise
423
665
  Without a server span, or with Effect's native tracer, whose spans go nowhere, the adapter adds no
424
666
  spans and no cost.
425
667
 
668
+ The adapter sets `http.route` on the server span to the pattern of the matched routes, such as
669
+ `/todos/:id`, once per request. It does this also for a page that runs no route function. A router
670
+ middleware that opens a span of its own does not change that: the pattern still goes on the server
671
+ span. A request that matches no route keeps the router's `*`. `ReactRouter.layer` reads the tracer
672
+ once, at layer build. So it does not see a tracer that the app provides only inside a request.
673
+
426
674
  ## Route functions
427
675
 
428
676
  ### Plain Effect loaders, actions and middleware
@@ -455,10 +703,10 @@ export const middleware = [
455
703
  `Route.ComponentProps` types the data of an Effect loader or action, including what the factory
456
704
  recovers.
457
705
  - Hooks that take `typeof loader` or `typeof action`, such as `useFetcher<typeof action>()`,
458
- read the data through React Router's `SerializeFrom`. It treats a function as a client
459
- function when its argument has the members of client loader args, and gives client functions
460
- their raw return value. Typegen's `Route.LoaderArgs` and `Route.ActionArgs` have those same
461
- members, so for an Effect the hook would type the data as the Effect itself.
706
+ read the data through React Router's `SerializeFrom`. If the argument of a function has the
707
+ members of client loader args, `SerializeFrom` treats it as a client function. It gives a
708
+ client function its raw return value. Typegen's `Route.LoaderArgs` and `Route.ActionArgs` have
709
+ those same members, so for an Effect the hook types the data as the Effect itself.
462
710
  - Take the args through `EffectRouteArgs<Route.LoaderArgs>`. It makes `pattern` optional, so the
463
711
  argument no longer matches and the hook reads the brand. A loader without args needs nothing.
464
712
  - `RouteData<typeof loader>` reads the brand directly and applies React Router's serialization.
@@ -468,8 +716,8 @@ export const middleware = [
468
716
  and the span that it provides around `next()` reach every route Effect downstream. `context.set`
469
717
  keeps working. You cannot annotate a route array with an effect middleware as
470
718
  `Route.MiddlewareFunction[]`.
471
- - Action metadata: `actionMeta` in the factory config is read after every action resolves. The
472
- adapter attaches it to the action's data under `__meta`.
719
+ - Route metadata: `routeMeta` in the factory configuration is read after every loader and action
720
+ resolves. The adapter attaches it to their data under `__meta`.
473
721
  - Tests: `routeFactory.handlers.loader(loader)(args)` runs a route function as the server does.
474
722
 
475
723
  ### Register what the factory recovers
@@ -480,7 +728,7 @@ A plain Effect loader cannot see the factory's handlers. Declare what they recov
480
728
  declare module "react-router-effect" {
481
729
  interface Register {
482
730
  recover: { FormValidationError: { reply: SubmissionResponse } };
483
- actionMeta: { realtimeEventIds: ReadonlyArray<string> };
731
+ routeMeta: { realtimeEventIds: ReadonlyArray<string> };
484
732
  }
485
733
  }
486
734
  ```
@@ -510,8 +758,10 @@ Type '{}' is missing the following properties from type ...:
510
758
 
511
759
  ### Requirements and limits
512
760
 
513
- - Effect 4.1 or later, React Router 8.3 or later for the adapter, and `skipLibCheck`. The brand
514
- augments `Effect`, and TypeScript cannot test the variance annotations of that augmentation.
761
+ - Effect 4.1 or later, and `skipLibCheck`. The brand augments `Effect`, and TypeScript cannot test
762
+ the variance annotations of that augmentation.
763
+ - For the adapter, React Router 8.3 or later. The route factory, `makeLoader`, `makeAction` and
764
+ `makeMiddleware` also support React Router 7.16 or later.
515
765
  - A route module export must be a function. A bare `Effect` value is not a loader.
516
766
  - Prerendering (`prerender`) and RSC mode call the build without the adapter.
517
767
 
@@ -594,9 +844,10 @@ of a rejected promise. It flows through the same recover and throw handling as a
594
844
 
595
845
  ### Per-request services
596
846
 
597
- Without the adapter there is no `requestContext` option. Set the context key in a plain React Router
598
- middleware. The factory's `requestContext` reads it, and provides those services to the loader or
599
- action:
847
+ Without the adapter, no router middleware runs before React Router. Set the context key in a plain
848
+ React Router middleware. The factory's `requestContext` reads it, and provides those services to the loader or
849
+ action. With the adapter, do not set the key this way, as
850
+ [Per-request services](#per-request-services) tells. This is the pattern without the adapter:
600
851
 
601
852
  ```ts
602
853
  // app/request-context.server.ts
@@ -635,8 +886,8 @@ export const loader = makeLoader(() =>
635
886
 
636
887
  ## API
637
888
 
638
- - `makeEffectRouteFactory<DomainErrors>()((Respond) => ({ errorHandlers?, runtime?, requestContext?, respond?, actionMeta? }))`
639
- returns `{ makeLoader, makeAction, makeMiddleware, Respond, handlers }`. The config is a builder
889
+ - `makeEffectRouteFactory<DomainErrors>()((Respond) => ({ errorHandlers?, runtime?, requestContext?, respond?, routeMeta? }))`
890
+ returns `{ makeLoader, makeAction, makeMiddleware, Respond, defer, handlers }`. The config is a builder
640
891
  that receives the base `Respond`. All its fields are optional. The returned `Respond` is the base
641
892
  helpers merged with your `respond` extensions, and the base helpers win. `makeLoader` and
642
893
  `makeAction` are the same wrapper. `makeMiddleware` wraps a `Route.MiddlewareFunction`, and
@@ -644,6 +895,9 @@ export const loader = makeLoader(() =>
644
895
  - `handlers` (`loader`, `action`, `middleware`, `requestContext`) is the runner that the server
645
896
  adapter applies to every route module.
646
897
  - `Respond` has `early` (recover), `throw` and `redirect`, and the `respond` helpers that you add.
898
+ - `defer` (unstable) starts an Effect and gives a `DeferredPromise` of its result for loader data.
899
+ Read [Defer data](#defer-data). The types `DeferredPromise`, `DeferDiagnose`, `DeferHandled` and
900
+ `DeferRecoverOf`, and the constant `deferredFailureMessage`, go with it.
647
901
  - `ReturnableDataError`, `ThrowableDataError` and `ThrowableRedirectError` are the library route
648
902
  errors. `isRouteError` narrows them.
649
903
  - `ErrorHandler<Err>` is the handler signature type.
@@ -651,10 +905,12 @@ export const loader = makeLoader(() =>
651
905
  effect context.
652
906
  - `EffectRouteArgs<Args>`, `EffectMiddlewareArgs<Middleware>`, `RouteData<F>` and `Register` are the
653
907
  types of plain Effect routes.
654
- - `react-router-effect/server` exports `ReactRouterHttp.make`, and the types `RouteEffectCheck` and
908
+ - `react-router-effect/server` (unstable) exports `ReactRouter`, with `ReactRouter.layer`, `ReactRouter.routes`
909
+ and the `ReactRouter.Options` type, and `ReactRouterAssets`, with `ReactRouterAssets.layer` and
910
+ `ReactRouterAssets.ClientBuildError`. It also exports the types `RouteEffectCheck` and
655
911
  `RouteEffectFailures`.
656
- - `react-router-effect/vite` exports `ReactRouterVite.devMiddleware`, for development. It needs
657
- `@effect/platform-node`.
912
+ - `react-router-effect/vite` (unstable) exports `ReactRouterVite.layer` and the `ReactRouterVite.ViteDevServer`
913
+ type, for development on Node. It needs `@effect/platform-node`.
658
914
 
659
915
  ## Development
660
916