@weftui/dom 0.28.0 → 0.29.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.
Files changed (38) hide show
  1. package/README.md +25 -14
  2. package/dist/boundary-replay-BY4GyLot.js +1 -0
  3. package/dist/client/index.d.ts +5 -5
  4. package/dist/client/index.js +1 -1
  5. package/dist/index.d.ts +195 -1
  6. package/dist/index.js +1 -1
  7. package/dist/server/index.d.ts +6 -6
  8. package/dist/server/index.js +1 -1
  9. package/dist/shared-Dz0KM9ku.js +1 -0
  10. package/docs/explanation/boundaries-and-suspense.md +37 -18
  11. package/docs/explanation/combinator-api.md +14 -12
  12. package/docs/explanation/reactive-primitives.md +14 -14
  13. package/docs/explanation/rendering-model.md +20 -18
  14. package/docs/explanation/services-and-context.md +35 -21
  15. package/docs/how-to/add-routing.md +72 -48
  16. package/docs/how-to/author-components.md +40 -28
  17. package/docs/how-to/compose-behavior-and-markup.md +144 -0
  18. package/docs/how-to/handle-forms.md +6 -6
  19. package/docs/how-to/load-async-data.md +15 -13
  20. package/docs/how-to/load-data-with-rpc.md +30 -28
  21. package/docs/how-to/provide-services.md +20 -18
  22. package/docs/how-to/render-keyed-lists.md +10 -8
  23. package/docs/how-to/render-on-the-server.md +16 -12
  24. package/docs/how-to/show-navigation-progress.md +10 -8
  25. package/docs/how-to/split-routes-lazily.md +16 -14
  26. package/docs/how-to/style-reactively.md +13 -13
  27. package/docs/how-to/use-element-refs.md +10 -8
  28. package/docs/index.md +20 -18
  29. package/docs/reference/core.md +44 -40
  30. package/docs/reference/dom.md +274 -58
  31. package/docs/reference/router.md +69 -47
  32. package/docs/tutorial/01-your-first-app.md +7 -9
  33. package/docs/tutorial/02-reactivity.md +8 -6
  34. package/docs/tutorial/03-services-and-async.md +10 -6
  35. package/docs/tutorial/04-errors-and-server.md +14 -5
  36. package/package.json +3 -3
  37. package/dist/boundary-replay-BR26_puM.js +0 -1
  38. package/dist/data-uLmMpQMV.js +0 -1
@@ -25,7 +25,7 @@ const LoginForm = () =>
25
25
  const email = yield* SubscriptionRef.make("");
26
26
  const status = yield* SubscriptionRef.make<string | null>(null);
27
27
 
28
- // Validation is a stream derived from the field it re-runs as the user types.
28
+ // Validation is a stream derived from the field: it re-runs as the user types.
29
29
  const error = Stream.map(SubscriptionRef.changes(email), (value) => {
30
30
  if (value.length === 0) return null; // don't nag an empty field
31
31
  return Result.match(Schema.decodeUnknownResult(Email)(value), {
@@ -61,8 +61,8 @@ const LoginForm = () =>
61
61
  ## How it works
62
62
 
63
63
  - **Field state** is a `SubscriptionRef.make("")`; `oninput` writes the current value with `SubscriptionRef.set`. Because the input is driven by the ref, it is a controlled input.
64
- - **Validation is reactive**, not on-blur or on-submit only: `Stream.map(SubscriptionRef.changes(email), …)` produces an error string (or `null`) on every keystroke. Use [`Schema`](https://effect.website/docs/schema/introduction) to decode `Schema.decodeUnknownResult(schema)(value)` returns a `Result`, and `Result.match` turns it into UI. A node or `null` in a child slot renders the error or nothing.
65
- - **Submit returns an Effect.** `onsubmit` calls `e.preventDefault()` and then **returns** an `Effect` (it is not `yield*`-ed inline) the renderer runs it in a detached fiber, so it can `SubscriptionRef.set`, `Effect.sleep`, read fields with `SubscriptionRef.get`, or call a service.
64
+ - **Validation is reactive**, not on-blur or on-submit only: `Stream.map(SubscriptionRef.changes(email), …)` produces an error string (or `null`) on every keystroke. Use [`Schema`](https://effect.website/docs/schema/introduction) to decode: `Schema.decodeUnknownResult(schema)(value)` returns a `Result`, and `Result.match` turns it into UI. A node or `null` in a child slot renders the error or nothing.
65
+ - **Submit returns an Effect.** `onsubmit` calls `e.preventDefault()` and then **returns** an `Effect` (it is not `yield*`-ed inline). The renderer runs it in a detached fiber, so it can `SubscriptionRef.set`, `Effect.sleep`, read fields with `SubscriptionRef.get`, or call a service.
66
66
 
67
67
  ## Variations
68
68
 
@@ -71,6 +71,6 @@ const LoginForm = () =>
71
71
 
72
72
  ## See also
73
73
 
74
- - [Reactive Primitives](https://weftui.dev/docs/explanation/reactive-primitives) `SubscriptionRef.changes` and stream-shaped children
75
- - [Author Components](https://weftui.dev/docs/how-to/author-components) Effect-returning and service-aware handlers
76
- - [examples/form-handling](https://github.com/stefvw93/weft/tree/main/examples/form-handling) a runnable multi-field form with Schema validation and an async submit
74
+ - [Reactive Primitives](https://weftui.dev/docs/explanation/reactive-primitives): `SubscriptionRef.changes` and stream-shaped children
75
+ - [Author Components](https://weftui.dev/docs/how-to/author-components): Effect-returning and service-aware handlers
76
+ - [examples/form-handling](https://github.com/stefvw93/weft/tree/main/examples/form-handling): a runnable multi-field form with Schema validation and an async submit
@@ -2,12 +2,12 @@
2
2
  title: Load Async Data
3
3
  order: 9
4
4
  section: how-to
5
- description: Show a loading state then resolved content with Stream.concat, and turn a failed fetch into a fallback node with Effect.catch all client-side.
5
+ description: Show a loading state then resolved content with Stream.concat, and turn a failed fetch into a fallback node with Effect.catch, all client-side.
6
6
  ---
7
7
 
8
8
  # Load Async Data
9
9
 
10
- **Goal:** render a loading placeholder, then the fetched content, and a fallback if the fetch fails for data that loads **on the client**.
10
+ **Goal:** render a loading placeholder, then the fetched content, and a fallback if the fetch fails, for data that loads **on the client**.
11
11
 
12
12
  Return a `Stream<Node>` that emits the loading node first and the resolved node second, sequenced with `Stream.concat`. Handle failure inside the effect with `Effect.catch`, which maps the error to a fallback node.
13
13
 
@@ -43,28 +43,30 @@ const UserCard = ({ id }: { id: number }) =>
43
43
  ## How it works
44
44
 
45
45
  - **`Stream.concat`** sequences two streams: `Stream.make(loadingNode)` emits once immediately, then `Stream.fromEffect(effect)` emits the resolved node when the effect completes. The renderer swaps the DOM in place on the second emission.
46
- - **`Effect.flatMap((data) => h.div(...))`** builds the content node from the data `h.*` returns a `Node`, which is an `Effect`, so it composes directly in the pipeline.
46
+ - **`Effect.flatMap((data) => h.div(...))`** builds the content node from the data. `h.*` returns a `Node`, which is an `Effect`, so it composes directly in the pipeline.
47
47
  - **`Effect.catch((error) => node)`** converts the error channel into a fallback node, so the stream always yields something renderable. The failure never escapes to the mount.
48
- - **Parallel loading is automatic:** place several async components as siblings and their fetches run concurrently no orchestration needed.
48
+ - **Parallel loading is automatic:** place several async components as siblings and their fetches run concurrently, with no orchestration needed.
49
49
 
50
50
  ## When to reach for a boundary instead
51
51
 
52
- This is the raw, per-region pattern. When you need **one fallback for several async siblings** (all-or-nothing), use [`Boundary.suspend`](https://weftui.dev/docs/explanation/boundaries-and-suspense). When the data must be resolved on the **server** and replayed on hydrate without a second request, use [`Boundary.rpc`](https://weftui.dev/docs/how-to/load-data-with-rpc) instead this recipe is purely client-side.
52
+ This is the raw, per-region pattern. When you need **one fallback for several async siblings** (all-or-nothing), use [`Boundary.suspend`](https://weftui.dev/docs/explanation/boundaries-and-suspense). When the data must be resolved on the **server** and replayed on hydrate without a second request, use [`Boundary.rpc`](https://weftui.dev/docs/how-to/load-data-with-rpc) instead. This recipe is purely client-side.
53
53
 
54
54
  ## Blocking on navigation vs streaming in place
55
55
 
56
- The `Stream.concat` placeholder above lives on a **child** node, so it always streams in after mount it never delays a navigation commit. If the component above is a route's leaf, moving the `fetchUser` call into the **body** instead changes that:
56
+ The `Stream.concat` placeholder above lives on a **child** node, so it always streams in after mount and never delays a navigation commit. If the component above is a route's leaf, moving the `fetchUser` call into the **body** instead changes that:
57
57
 
58
- - **Await in the leaf's own body** commit-blocking. Navigating to the route pre-runs its component effect to completion before the URL commits: the previous page stays mounted for the fetch, and [`Router.navigating`](https://weftui.dev/docs/reference/router#routernavigating) reports the window.
59
- - **The `Stream.concat` placeholder pattern above, kept as a child** streaming. The leaf commits immediately and the region fills in place once the effect resolves.
58
+ - **Await in the leaf's own body** is commit-blocking. Navigating to the route pre-runs its component effect to completion before the URL commits: the previous page stays mounted for the fetch, and [`Router.navigating`](https://weftui.dev/docs/reference/router#routernavigating) reports the window.
59
+ - **The `Stream.concat` placeholder pattern above, kept as a child** is streaming. The leaf commits immediately and the region fills in place once the effect resolves.
60
60
 
61
- Choose blocking for **primary route content the page is meaningless without** (an article body, a user's profile) the old page stays visible with no blank or skeleton. Choose streaming for **secondary or slow regions** where partial content is still useful (a comments panel, a "related" rail) — the commit isn't held hostage by one slow fetch.
61
+ Choose blocking for **primary route content the page is meaningless without** (an article body, a user's profile). The old page stays visible with no blank or skeleton.
62
+
63
+ Choose streaming for **secondary or slow regions** where partial content is still useful (a comments panel, a "related" rail). The commit isn't held hostage by one slow fetch.
62
64
 
63
65
  See [Show Navigation Progress](https://weftui.dev/docs/how-to/show-navigation-progress) for rendering pending UI during the blocking window, and the router reference's [Blocking vs streaming data](https://weftui.dev/docs/reference/router#blocking-vs-streaming-data) for the full model.
64
66
 
65
67
  ## See also
66
68
 
67
- - [Boundaries and Suspense](https://weftui.dev/docs/explanation/boundaries-and-suspense) coordinating multiple async regions
68
- - [Reactive Primitives](https://weftui.dev/docs/explanation/reactive-primitives) `Stream`/`Effect` as node-producing children
69
- - [Show Navigation Progress](https://weftui.dev/docs/how-to/show-navigation-progress) the pending signal for the commit-blocking window
70
- - [examples/async-data-loading](https://github.com/stefvw93/weft/tree/main/examples/async-data-loading) loading states, retry, parallel and sequential loads with error boundaries
69
+ - [Boundaries and Suspense](https://weftui.dev/docs/explanation/boundaries-and-suspense): coordinating multiple async regions
70
+ - [Reactive Primitives](https://weftui.dev/docs/explanation/reactive-primitives): `Stream`/`Effect` as node-producing children
71
+ - [Show Navigation Progress](https://weftui.dev/docs/how-to/show-navigation-progress): the pending signal for the commit-blocking window
72
+ - [examples/async-data-loading](https://github.com/stefvw93/weft/tree/main/examples/async-data-loading): loading states, retry, parallel and sequential loads with error boundaries
@@ -2,7 +2,7 @@
2
2
  title: RPC Data Boundaries
3
3
  order: 3
4
4
  section: how-to
5
- description: Boundary.rpc server-resolved, client-refreshable data; the contract/handler split and the Resource handle's four lifecycles.
5
+ description: Boundary.rpc, server-resolved and client-refreshable data; the contract/handler split and the Resource handle's four lifecycles.
6
6
  ---
7
7
 
8
8
  # RPC Data Boundaries
@@ -11,7 +11,7 @@ description: Boundary.rpc — server-resolved, client-refreshable data; the cont
11
11
 
12
12
  ## Overview
13
13
 
14
- A `Boundary.rpc` is a **thin consumer**. It carries an rpc, a payload thunk, and a `render` that receives a reactive [`Resource`](https://weftui.dev/docs/reference/core#resourcea); the renderer resolves the rpc through the ambient [`AppRpcClientTag`](https://weftui.dev/docs/reference/core#apprpcclienttag) seam (provided by `@weftui/router`). The same rpc serves every lifecycle, so SSR-replay, refetch, and client-first mount are one mechanism, not three.
14
+ A `Boundary.rpc` is a **thin consumer**. It carries an rpc, a payload thunk, and a `render` that receives a reactive [`Resource`](https://weftui.dev/docs/reference/core#resourcea). The renderer resolves the rpc through the ambient [`AppRpcClientTag`](https://weftui.dev/docs/reference/core#apprpcclienttag) seam (provided by `@weftui/router`). The same rpc serves every lifecycle, so SSR-replay, refetch, and client-first mount are one mechanism, not three.
15
15
 
16
16
  ```typescript
17
17
  import { Boundary, h } from "@weftui/core";
@@ -19,10 +19,10 @@ import { Stream } from "effect";
19
19
  import { GetStock } from "./data/inventory";
20
20
 
21
21
  Boundary.rpc(
22
- GetStock, // the rpc its _tag + schemas drive the boundary
23
- () => ({ id: product.id }), // payload thunk a fresh typed input per call
22
+ GetStock, // the rpc: its _tag + schemas drive the boundary
23
+ () => ({ id: product.id }), // payload thunk: a fresh typed input per call
24
24
  (
25
- resource, // render receives a reactive Resource, not a bare value
25
+ resource, // render: receives a reactive Resource, not a bare value
26
26
  ) =>
27
27
  h.p([
28
28
  "in stock: ",
@@ -35,7 +35,7 @@ Boundary.rpc(
35
35
 
36
36
  ## The contract / handler split
37
37
 
38
- The rpc **contract** (pure Schema) is shared with the client. The rpc **handler** the only code that touches server-only services lives in a Layer the client never imports; tree-shaking keeps it and its transitive imports out of the browser bundle. The split is enforced structurally by which files each entry imports, not by a bundler plugin.
38
+ The rpc **contract** (pure Schema) is shared with the client. The rpc **handler**, the only code that touches server-only services, lives in a Layer the client never imports. Tree-shaking keeps it and its transitive imports out of the browser bundle. The split is enforced structurally by which files each entry imports, not by a bundler plugin.
39
39
 
40
40
  ```typescript
41
41
  // data/inventory.ts
@@ -49,7 +49,7 @@ export const StockKey = Schema.Struct({ id: Schema.Number });
49
49
  // `_tag` ("GetStock") = the stable boundary id; payload schema = the typed input.
50
50
  export const GetStock = Rpc.make("GetStock", { payload: StockKey, success: Stock });
51
51
 
52
- // The app's merged RpcGroup shared by both the client and server router wiring.
52
+ // The app's merged RpcGroup: shared by both the client and server router wiring.
53
53
  export const StockRpcs = RpcGroup.make(GetStock);
54
54
 
55
55
  // --- Handler (server-only; the client never imports this) ---
@@ -68,14 +68,14 @@ export const StockLive = StockRpcs.toLayer({
68
68
  }).pipe(Layer.provide(InventoryLive));
69
69
  ```
70
70
 
71
- Declare server-only services with [`ServerTag`](https://weftui.dev/docs/reference/core#servertag) (not `Context.Service`) when they might be referenced from universal code: the brand makes a leak into `render` a compile error at the `hydrate` call site rather than a runtime surprise.
71
+ Declare server-only services with [`ServerTag`](https://weftui.dev/docs/reference/core#servertag) (not `Context.Service`) when they might be referenced from universal code. The brand makes a leak into `render` a compile error at the `hydrate` call site rather than a runtime surprise.
72
72
 
73
73
  ## Wiring the router
74
74
 
75
75
  `Boundary.rpc` resolves through the ambient `AppRpcClientTag` seam, which `@weftui/router` provides on both sides. Pass the **merged group** to both, plus the **handler Layer** on the server.
76
76
 
77
77
  ```typescript
78
- // entry-server.ts in-process client over the handlers + POST /_eui/rpc endpoint
78
+ // entry-server.ts: in-process client over the handlers + POST /_eui/rpc endpoint
79
79
  import { RouterServer } from "@weftui/router/server";
80
80
  import { StockLive, StockRpcs } from "./data/inventory";
81
81
 
@@ -87,7 +87,7 @@ export const render = (url: string) =>
87
87
  ```
88
88
 
89
89
  ```typescript
90
- // entry-client.ts network client posting to /_eui/rpc
90
+ // entry-client.ts: network client posting to /_eui/rpc
91
91
  import { WeftApp } from "@weftui/dom/client";
92
92
  import { RouterApp, RouterLive } from "@weftui/router/client";
93
93
  import { Effect } from "effect";
@@ -97,7 +97,7 @@ const app = WeftApp.make(RouterLive(App, { rpc: { group: StockRpcs } }));
97
97
  void Effect.runPromise(WeftApp.hydrate(app, RouterApp(App), root));
98
98
  ```
99
99
 
100
- - **Server** ([`RouterServer`](https://weftui.dev/docs/reference/router#routerserver)) mounts the handler Layer at `POST /_eui/rpc` (so a client refetch re-runs it on the server) **and** exposes an in-process client over the same handlers for SSR resolution never a network hop.
100
+ - **Server** ([`RouterServer`](https://weftui.dev/docs/reference/router#routerserver)) mounts the handler Layer at `POST /_eui/rpc`, so a client refetch re-runs it on the server. It also exposes an in-process client over the same handlers for SSR resolution, never a network hop.
101
101
  - **Client** ([`RouterLive`](https://weftui.dev/docs/reference/router#routerlive)) provides a network flat rpc client over the merged group, posting to `<origin>/_eui/rpc`.
102
102
 
103
103
  In a **router-less mount** there is no `AppRpcClientTag`, so a `Boundary.rpc` resolves to a typed, descriptive "needs router/rpc" error (not a defect).
@@ -111,18 +111,18 @@ In a **router-less mount** there is no `AppRpcClientTag`, so a `Boundary.rpc` re
111
111
  | **Refetch** | `resource.refetch` | Call the rpc again over `POST /_eui/rpc` (re-runs the handler on the server), patch the subtree in place (stale-on-error). |
112
112
  | **Client-first mount** | SPA nav into a boundary with no payload | Render `options.fallback`, fork the rpc call, swap in `render(resource)` once it resolves. |
113
113
 
114
- Because the SSR path seeds `value` await-first (it emits the seed immediately), the SSR HTML and the adopted DOM are byte-identical there is **no fallback flash** on the SSR/hydrate path. `fallback` shows only on a client-first mount.
114
+ Because the SSR path seeds `value` await-first (it emits the seed immediately), the SSR HTML and the adopted DOM are byte-identical. There is **no fallback flash** on the SSR/hydrate path. `fallback` shows only on a client-first mount.
115
115
 
116
116
  ## The `Resource` handle
117
117
 
118
118
  `render` receives a [`Resource<A>`](https://weftui.dev/docs/reference/core#resourcea) (`A` = the rpc's decoded success), not a bare value. After hydrate the region is live:
119
119
 
120
- | Field | What it gives you |
121
- | --------- | ---------------------------------------------------------------------------------------------------- |
122
- | `value` | A `Subscribable` of the current data seeded with the SSR payload, updated on a successful refetch. |
123
- | `refetch` | An `Effect<void>` that re-resolves the rpc with a fresh `payload()` and pushes the new `value`. |
124
- | `pending` | A `Subscribable<boolean>` `true` while a refetch is in flight. |
125
- | `error` | A `Subscribable<Option<unknown>>` `Some` with the last refetch error (stale-on-error). |
120
+ | Field | What it gives you |
121
+ | --------- | --------------------------------------------------------------------------------------------------- |
122
+ | `value` | A `Subscribable` of the current data: seeded with the SSR payload, updated on a successful refetch. |
123
+ | `refetch` | An `Effect<void>` that re-resolves the rpc with a fresh `payload()` and pushes the new `value`. |
124
+ | `pending` | A `Subscribable<boolean>`: `true` while a refetch is in flight. |
125
+ | `error` | A `Subscribable<Option<unknown>>`: `Some` with the last refetch error (stale-on-error). |
126
126
 
127
127
  ```typescript
128
128
  (resource) =>
@@ -133,7 +133,7 @@ Because the SSR path seeds `value` await-first (it emits the seed immediately),
133
133
  ]);
134
134
  ```
135
135
 
136
- Wire `refetch` to an event with `onclick: () => resource.refetch` the handler returns the Effect, which the renderer runs in a detached fiber. A failed refetch leaves the previous `value` intact (stale-on-error); it does **not** unmount the subtree or raise into a failure `Boundary`.
136
+ Wire `refetch` to an event with `onclick: () => resource.refetch`. The handler returns the Effect, which the renderer runs in a detached fiber. A failed refetch leaves the previous `value` intact (stale-on-error); it does **not** unmount the subtree or raise into a failure `Boundary`.
137
137
 
138
138
  ### Channel algebra
139
139
 
@@ -147,11 +147,13 @@ Boundary.rpc<R extends Rpc.Any, C extends Node<any, any>>(
147
147
  ```
148
148
 
149
149
  - **Error** = `render`'s error union plus the rpc's typed `Rpc.Error<R>` (`never` for an rpc with no `error` schema).
150
- - **Requirement** = exactly `render`'s `R`, **untouched**. There is no `provide`/`RServer` to discharge (the handler lives in the rpc Layer) and no `Exclude` is applied a server-only tag leaked into `render` stays in `R`, where `hydrate`'s `AssertNoServerOnly` rejects it.
150
+ - **Requirement** = exactly `render`'s `R`, **untouched**. There is no `provide`/`RServer` to discharge (the handler lives in the rpc Layer) and no `Exclude` is applied. A server-only tag leaked into `render` stays in `R`, where `hydrate`'s `AssertNoServerOnly` rejects it.
151
151
 
152
152
  ## Typed-failure replay
153
153
 
154
- If the rpc declares an `error` schema, a resolved rpc **error** on the SSR pass is `errorSchema`-encoded into an inline failure payload, and the nearest enclosing **failure `Boundary`** renders its fallback. On the client, `hydrate` decodes that payload and re-raises the same error into the same boundary, reproducing the identical fallback DOM — flash-free and without re-resolving the rpc (replay, never retry).
154
+ If the rpc declares an `error` schema, a resolved rpc **error** on the SSR pass is `errorSchema`-encoded into an inline failure payload. The nearest enclosing **failure `Boundary`** renders its fallback.
155
+
156
+ On the client, `hydrate` decodes that payload and re-raises the same error into the same boundary. The identical fallback DOM is reproduced, flash-free and without re-resolving the rpc (replay, never retry).
155
157
 
156
158
  ```typescript
157
159
  Boundary.catchTag({ tag: "OutOfStock", fallback: (e) => h.p({ class: "error" }, e.reason) }, [
@@ -159,16 +161,16 @@ Boundary.catchTag({ tag: "OutOfStock", fallback: (e) => h.p({ class: "error" },
159
161
  ]);
160
162
  ```
161
163
 
162
- A transport **defect** (no `Cause.findErrorOption`), or an rpc with no `error` schema, is **not** replayed; it propagates a server-side fallback and a client mismatch.
164
+ A transport **defect** (no `Cause.findErrorOption`), or an rpc with no `error` schema, is **not** replayed; it propagates: a server-side fallback and a client mismatch.
163
165
 
164
166
  ## When to use
165
167
 
166
- - **`Boundary.rpc`** data resolved on the server (behind a server-only service, credential, or private network) and rendered into the initial HTML, then **refreshable** on the client over the same rpc.
167
- - **`Boundary.suspend`** async data that loads purely on the client; see the [Boundary API](https://weftui.dev/docs/reference/core#boundarysuspend).
168
+ - **`Boundary.rpc`**: data resolved on the server (behind a server-only service, credential, or private network) and rendered into the initial HTML. It stays **refreshable** on the client over the same rpc.
169
+ - **`Boundary.suspend`**: async data that loads purely on the client; see the [Boundary API](https://weftui.dev/docs/reference/core#boundarysuspend).
168
170
 
169
171
  ## See also
170
172
 
171
- - [`Boundary.rpc` API reference](https://weftui.dev/docs/reference/core#boundaryrpc) signature, `Resource`, `RpcOptions`, `AppRpcClientTag`
172
- - [Server-Side Rendering](https://weftui.dev/docs/how-to/render-on-the-server) the SSR + hydration model this builds on
173
- - [Routing](https://weftui.dev/docs/how-to/add-routing) `@weftui/router`, which provides the `AppRpcClientTag` seam
174
- - [examples/router-ssr](https://github.com/stefvw93/weft/tree/main/examples/router-ssr) a runnable shop with an SSR-replayed, refetchable live-stock `Boundary.rpc`
173
+ - [`Boundary.rpc` API reference](https://weftui.dev/docs/reference/core#boundaryrpc): signature, `Resource`, `RpcOptions`, `AppRpcClientTag`
174
+ - [Server-Side Rendering](https://weftui.dev/docs/how-to/render-on-the-server): the SSR + hydration model this builds on
175
+ - [Routing](https://weftui.dev/docs/how-to/add-routing): `@weftui/router`, which provides the `AppRpcClientTag` seam
176
+ - [examples/router-ssr](https://github.com/stefvw93/weft/tree/main/examples/router-ssr): a runnable shop with an SSR-replayed, refetchable live-stock `Boundary.rpc`
@@ -2,16 +2,16 @@
2
2
  title: Provide Services
3
3
  order: 12
4
4
  section: how-to
5
- description: Provide plain and scoped Layers to a WeftApp app layers for the common case, scoped layers that just work, memoMap sharing, and binding an app's lifetime to a scope.
5
+ description: "Provide plain and scoped Layers to a WeftApp: app layers for the common case, scoped layers that just work, memoMap sharing, and binding an app's lifetime to a scope."
6
6
  ---
7
7
 
8
8
  # Provide Services
9
9
 
10
10
  **Goal:** provide a `Layer` to a `WeftApp` so its components can read services with `yield* Service`.
11
11
 
12
- ## Recipe 1 app layers
12
+ ## Recipe 1: app layers
13
13
 
14
- Pass the layer to `WeftApp.make`. This is the common case and needs nothing else the layer builds lazily on first mount, and every component, event handler, and stream subscription in every root mounted from `app` can read it.
14
+ Pass the layer to `WeftApp.make`. This is the common case and needs nothing else. The layer builds lazily on first mount, and every component, event handler, and stream subscription in every root mounted from `app` can read it.
15
15
 
16
16
  ```typescript
17
17
  import { WeftApp } from "@weftui/dom/client";
@@ -25,11 +25,13 @@ const app = WeftApp.make(ThemeServiceLive);
25
25
  void Effect.runPromise(WeftApp.mount(app, App(), root));
26
26
  ```
27
27
 
28
- ## Recipe 2 scoped layers just work
28
+ ## Recipe 2: scoped layers just work
29
29
 
30
- A **scoped** layer `Layer.effect` backed by `acquireRelease`, or anything else that owns a subscription, listener, or registry needs nothing different from Recipe 1. The app owns one lazy `ManagedRuntime`: the layer builds on first mount and releases only at `WeftApp.dispose(app)`, not when any individual mount's render effect resolves. There is no `mountScoped`, no `Effect.never`, no manual scope threading.
30
+ A **scoped** layer (`Layer.effect` backed by `acquireRelease`, or anything else that owns a subscription, listener, or registry) needs nothing different from Recipe 1. The app owns one lazy `ManagedRuntime`. The layer builds on first mount and releases only at `WeftApp.dispose(app)`, not when any individual mount's render effect resolves.
31
31
 
32
- `AtomRegistry.layer` (from `effect/unstable/reactivity`) is a real scoped layer its atom subscriptions are fibers forked for the app's whole lifetime:
32
+ There is no `mountScoped`, no `Effect.never`, no manual scope threading.
33
+
34
+ `AtomRegistry.layer` (from `effect/unstable/reactivity`) is a real scoped layer. Its atom subscriptions are fibers forked for the app's whole lifetime:
33
35
 
34
36
  ```typescript
35
37
  import { WeftApp } from "@weftui/dom/client";
@@ -41,7 +43,7 @@ const app = WeftApp.make(AtomRegistry.layer);
41
43
  void Effect.runPromise(WeftApp.mount(app, App(), document.getElementById("root")!));
42
44
  ```
43
45
 
44
- `RouterLive` (from `@weftui/router/client`) is another it owns the `popstate` listener and the same-origin link-click interceptor for as long as the app runs:
46
+ `RouterLive` (from `@weftui/router/client`) is another. It owns the `popstate` listener and the same-origin link-click interceptor for as long as the app runs:
45
47
 
46
48
  ```typescript
47
49
  const app = WeftApp.make(RouterLive(App, { rpc: { group: StockRpcs } }));
@@ -50,9 +52,9 @@ void Effect.runPromise(WeftApp.hydrate(app, RouterApp(App), root));
50
52
 
51
53
  Both examples are runnable in full at [examples/effect-atom](https://github.com/stefvw93/weft/tree/main/examples/effect-atom) and [examples/router-ssr](https://github.com/stefvw93/weft/tree/main/examples/router-ssr).
52
54
 
53
- ## Recipe 3 sharing layer memoization with `memoMap`
55
+ ## Recipe 3: sharing layer memoization with `memoMap`
54
56
 
55
- `WeftApp.make(layer, { memoMap })` accepts an explicit `Layer.MemoMap`, so multiple `WeftApp` instances can share layer construction for example, building one app per test case while reusing an expensive shared dependency's memoized build across them:
57
+ `WeftApp.make(layer, { memoMap })` accepts an explicit `Layer.MemoMap`, so multiple `WeftApp` instances can share layer construction. For example, build one app per test case while reusing an expensive shared dependency's memoized build across them:
56
58
 
57
59
  ```typescript
58
60
  import { WeftApp } from "@weftui/dom/client";
@@ -66,9 +68,9 @@ const appB = WeftApp.make(SharedLive, { memoMap });
66
68
 
67
69
  Most apps have exactly one `WeftApp` and never need this option.
68
70
 
69
- ## Recipe 4 binding an app's lifetime to a scope
71
+ ## Recipe 4: binding an app's lifetime to a scope
70
72
 
71
- There is deliberately no `makeScoped`. To tie an app's disposal to a `Scope` you already manage a framework integration or a test harness that owns one compose it yourself with `Effect.acquireRelease`:
73
+ There is deliberately no `makeScoped`. To tie an app's disposal to a `Scope` you already manage (a framework integration or a test harness that owns one), compose it yourself with `Effect.acquireRelease`:
72
74
 
73
75
  ```typescript
74
76
  import { Effect } from "effect";
@@ -81,22 +83,22 @@ const acquireApp = Effect.acquireRelease(
81
83
  );
82
84
  ```
83
85
 
84
- `acquireApp` yields a `WeftApp` and registers `WeftApp.dispose` as a finalizer on whatever scope the surrounding effect runs in closing that scope tears the app down the same way `WeftApp.dispose` normally would (roots, then layers, then the error hub).
86
+ `acquireApp` yields a `WeftApp` and registers `WeftApp.dispose` as a finalizer on whatever scope the surrounding effect runs in. Closing that scope tears the app down the same way `WeftApp.dispose` normally would (roots, then layers, then the error hub).
85
87
 
86
88
  ## Anti-pattern: `Effect.provide` around the mount call
87
89
 
88
90
  ```typescript
89
91
  // ❌ does nothing useful: WeftApp.mount's R is always `never`, and services
90
- // come exclusively from the app layer a wrapped Effect.provide never
92
+ // come exclusively from the app layer: a wrapped Effect.provide never
91
93
  // reaches components, handlers, or stream subscriptions
92
94
  Effect.runPromise(pipe(WeftApp.mount(app, App(), root), Effect.provide(SomeLayer)));
93
95
  ```
94
96
 
95
- `WeftApp.mount`/`WeftApp.hydrate` return an effect whose requirement channel is always `never` there is no `R` left for `Effect.provide` to discharge. Any service a component needs must be in the layer passed to `WeftApp.make`.
97
+ `WeftApp.mount`/`WeftApp.hydrate` return an effect whose requirement channel is always `never`, so there is no `R` left for `Effect.provide` to discharge. Any service a component needs must be in the layer passed to `WeftApp.make`.
96
98
 
97
99
  ## See also
98
100
 
99
- - [Services and Context](https://weftui.dev/docs/explanation/services-and-context) how `R` accumulates and discharges at `WeftApp.make`, and why scoped layers no longer need special handling
100
- - [`WeftApp` reference](https://weftui.dev/docs/reference/dom) full signatures for `make`, `mount`, `hydrate`, `dispose`
101
- - [examples/effect-atom](https://github.com/stefvw93/weft/tree/main/examples/effect-atom) a real scoped layer (`AtomRegistry.layer`)
102
- - [examples/shared-state-islands](https://github.com/stefvw93/weft/tree/main/examples/shared-state-islands) one app layer shared by reference across multiple mounted roots
101
+ - [Services and Context](https://weftui.dev/docs/explanation/services-and-context): how `R` accumulates and discharges at `WeftApp.make`, and why scoped layers no longer need special handling
102
+ - [`WeftApp` reference](https://weftui.dev/docs/reference/dom): full signatures for `make`, `mount`, `hydrate`, `dispose`
103
+ - [examples/effect-atom](https://github.com/stefvw93/weft/tree/main/examples/effect-atom): a real scoped layer (`AtomRegistry.layer`)
104
+ - [examples/shared-state-islands](https://github.com/stefvw93/weft/tree/main/examples/shared-state-islands): one app layer shared by reference across multiple mounted roots
@@ -9,7 +9,7 @@ description: Render a reactive collection with List.each so reordering, insertin
9
9
 
10
10
  **Goal:** render a list whose items reorder, insert, or remove over time, without rebuilding the whole region (which would lose focus, scroll, and input state in the surviving rows).
11
11
 
12
- Use [`List.each`](https://weftui.dev/docs/reference/core#listeach), the keyed-list combinator. It renders each item **once per key** and reconciles across emissions a reorder _moves_ existing DOM nodes, an insert adds one, a remove drops one, and untouched rows are left entirely alone.
12
+ Use [`List.each`](https://weftui.dev/docs/reference/core#listeach), the keyed-list combinator. It renders each item **once per key** and reconciles across emissions. A reorder _moves_ existing DOM nodes, an insert adds one, a remove drops one, and untouched rows are left entirely alone.
13
13
 
14
14
  ```typescript
15
15
  import { h, List } from "@weftui/core";
@@ -25,16 +25,18 @@ h.ul([
25
25
  ]);
26
26
  ```
27
27
 
28
- - **`of`** the list source: any `Stream`, `Effect`, or `Subscribable` of an `Iterable`. Each emission is materialized to an array to fix order, then reconciled by key.
29
- - **`by`** projects each item to its reconciliation key, compared via Effect's `Equal`/`Hash`. Omit it and the item itself is the key (structural for `Data`, by reference otherwise).
28
+ - **`of`** is the list source: any `Stream`, `Effect`, or `Subscribable` of an `Iterable`. Each emission is materialized to an array to fix order, then reconciled by key.
29
+ - **`by`** projects each item to its reconciliation key, compared via Effect's `Equal`/`Hash`. Omit it and the item itself is the key (structural for `Data`, by reference otherwise).
30
30
 
31
31
  ## Why not `map`?
32
32
 
33
- Mapping items by hand `Stream.map(rows.changes, (rs) => rs.map(r => h.li(r.name)))` produces a **new children array on every emission**, so the renderer rebuilds the whole region: every row's DOM node is recreated even if only one item moved. `List.each` reconciles by key instead, so DOM identity (and the focus/scroll/typed-input state attached to it) survives across updates.
33
+ Mapping items by hand (`Stream.map(rows.changes, (rs) => rs.map(r => h.li(r.name)))`) produces a **new children array on every emission**. The renderer then rebuilds the whole region: every row's DOM node is recreated even if only one item moved.
34
+
35
+ `List.each` reconciles by key instead, so DOM identity (and the focus/scroll/typed-input state attached to it) survives across updates.
34
36
 
35
37
  ## Refresh a row's content
36
38
 
37
- Because `render` runs **exactly once per key**, reconciliation never re-runs it for a kept row so it never refreshes that row's content on its own. To make a row's content reactive, thread a `Stream` **inside** the row rather than expecting a re-render:
39
+ Because `render` runs **exactly once per key**, reconciliation never re-runs it for a kept row, so it never refreshes that row's content on its own. To make a row's content reactive, thread a `Stream` **inside** the row rather than expecting a re-render:
38
40
 
39
41
  ```typescript
40
42
  List.each({ of: rows.changes, by: (row) => row.id }, (row) =>
@@ -46,6 +48,6 @@ List.each({ of: rows.changes, by: (row) => row.id }, (row) =>
46
48
 
47
49
  ## See also
48
50
 
49
- - [`List.each` API reference](https://weftui.dev/docs/reference/core#listeach) full signature, `List.Options`, and the descriptor shape
50
- - [Reactive Primitives](https://weftui.dev/docs/explanation/reactive-primitives) the stream-shaped sources `of` accepts
51
- - [examples/keyed-list](https://github.com/stefvw93/weft/tree/main/examples/keyed-list) a runnable keyed list with reordering and a browser test
51
+ - [`List.each` API reference](https://weftui.dev/docs/reference/core#listeach): full signature, `List.Options`, and the descriptor shape
52
+ - [Reactive Primitives](https://weftui.dev/docs/explanation/reactive-primitives): the stream-shaped sources `of` accepts
53
+ - [examples/keyed-list](https://github.com/stefvw93/weft/tree/main/examples/keyed-list): a runnable keyed list with reordering and a browser test
@@ -7,12 +7,14 @@ description: renderToString / renderToStringHydratable / streaming variants, hyd
7
7
 
8
8
  # Server-Side Rendering
9
9
 
10
- Weft renders on the server and **hydrates** on the client: the server produces HTML (plus inline data), and the browser adopts that existing DOM in place rather than re-creating it. [`Boundary.rpc`](https://weftui.dev/docs/reference/core#boundaryrpc) extends this to **rpc-backed server data** — resolve an rpc on the server, serialize its result into the HTML, replay it on the client without a second request, and then keep the region live for refetch.
10
+ Weft renders on the server and **hydrates** on the client. The server produces HTML plus inline data, and the browser adopts that existing DOM in place rather than re-creating it.
11
+
12
+ [`Boundary.rpc`](https://weftui.dev/docs/reference/core#boundaryrpc) extends this to **rpc-backed server data**: resolve an rpc on the server, serialize its result into the HTML, and replay it on the client without a second request. The region then stays live for refetch.
11
13
 
12
14
  ## The two halves
13
15
 
14
- - **Server** `@weftui/dom/server` renders an app node to an HTML string (or stream). The _hydratable_ variants additionally emit the inline data each reactive region and `Boundary.rpc` needs to resume on the client.
15
- - **Client** `@weftui/dom/client`'s `WeftApp.hydrate` walks the server DOM, adopts it, wires up reactivity and event handlers, and resumes from the inline data. It does **not** re-render from scratch.
16
+ - **Server**: `@weftui/dom/server` renders an app node to an HTML string (or stream). The _hydratable_ variants also emit the inline data each reactive region and `Boundary.rpc` needs to resume on the client.
17
+ - **Client**: `@weftui/dom/client`'s `WeftApp.hydrate` walks the server DOM, adopts it, wires up reactivity and event handlers, and resumes from the inline data. It does **not** re-render from scratch.
16
18
 
17
19
  ```typescript
18
20
  // server entry
@@ -34,7 +36,7 @@ const app = WeftApp.make();
34
36
  void Effect.runPromise(WeftApp.hydrate(app, App(), root));
35
37
  ```
36
38
 
37
- The same side-effect-free `App` is imported by both entries — splice the server HTML into your template's outlet, ship it, and let the client entry hydrate it.
39
+ Both entries import the same side-effect-free `App`. Splice the server HTML into your template's outlet, ship it, and let the client entry hydrate it.
38
40
 
39
41
  `@weftui/dom/server` exports four renderers:
40
42
 
@@ -47,7 +49,9 @@ Use a hydratable renderer whenever the client will call `hydrate`. The plain ren
47
49
 
48
50
  ## Loading server data with `Boundary.rpc`
49
51
 
50
- SSR's natural companion is `Boundary.rpc`: it resolves an rpc **on the server**, serializes the result into the same HTML this page produces, and replays it on the client during `hydrate` no second request, no fallback flash then keeps the region live for `refetch`. It is the data half of the same server/client split described above: the rpc **contract** (pure Schema) is shared, while its **handler** lives in a server-only Layer the client never imports.
52
+ `Boundary.rpc` resolves an rpc **on the server**, serializes the result into the same HTML this page produces, and replays it on the client during `hydrate`. There is no second request and no fallback flash, and the region stays live for `refetch`.
53
+
54
+ It follows the same server/client split: the rpc **contract** (pure Schema) is shared, while its **handler** lives in a server-only Layer the client never imports.
51
55
 
52
56
  ```typescript
53
57
  import { Boundary, h } from "@weftui/core";
@@ -68,20 +72,20 @@ const StockPanel = (productId: number) =>
68
72
  );
69
73
  ```
70
74
 
71
- Under SSR the server resolves the rpc in-process, `successSchema`-encodes the result inline as `<script type="application/json">`, and renders in place; `hydrate` reads that payload positionally, seeds the `Resource`, and adopts the DOM **without re-calling the rpc** (replay, never retry). The full model — the contract/handler split, router wiring, the four lifecycles, the `Resource` handle, and typed-failure replay — lives in one place: the [RPC Data Boundaries guide](https://weftui.dev/docs/how-to/load-data-with-rpc). This page does not repeat it.
75
+ Under SSR the server resolves the rpc in-process, `successSchema`-encodes the result inline as `<script type="application/json">`, and renders in place; `hydrate` reads that payload positionally, seeds the `Resource`, and adopts the DOM **without re-calling the rpc** (replay, never retry). The full model lives in one place, the [RPC Data Boundaries guide](https://weftui.dev/docs/how-to/load-data-with-rpc): the contract/handler split, router wiring, the four lifecycles, the `Resource` handle, and typed-failure replay. This page does not repeat it.
72
76
 
73
77
  > **Note.** `Boundary.rpc` resolves through the ambient [`AppRpcClientTag`](https://weftui.dev/docs/reference/core#apprpcclienttag) seam, which `@weftui/router` provides on both sides. In a router-less mount there is no seam, so the boundary resolves to a descriptive "needs router/rpc" error (not a defect).
74
78
 
75
79
  ## When to use
76
80
 
77
- - **`Boundary.rpc`** data that must be resolved on the server (behind a server-only service, credential, or private network) and rendered into the initial HTML, then **refreshable** on the client (refetch / client-first SPA mount) over the same rpc.
78
- - **`Boundary.suspend`** async data that loads on the client (or streams the shell then fills); see the [Boundary API](https://weftui.dev/docs/reference/core#boundarysuspend).
81
+ - **`Boundary.rpc`**: data that must be resolved on the server (behind a server-only service, credential, or private network) and rendered into the initial HTML, then **refreshable** on the client (refetch / client-first SPA mount) over the same rpc.
82
+ - **`Boundary.suspend`**: async data that loads on the client (or streams the shell then fills); see the [Boundary API](https://weftui.dev/docs/reference/core#boundarysuspend).
79
83
 
80
84
  ## See also
81
85
 
82
- - [rpc data boundaries guide](https://weftui.dev/docs/how-to/load-data-with-rpc) the full `Boundary.rpc` walkthrough: contract/handler split, router wiring, the four lifecycles, and typed-failure replay
83
- - [Routing](https://weftui.dev/docs/how-to/add-routing) `@weftui/router` builds on this SSR + hydration model for full-page nested routing
86
+ - [rpc data boundaries guide](https://weftui.dev/docs/how-to/load-data-with-rpc): the full `Boundary.rpc` walkthrough, covering the contract/handler split, router wiring, the four lifecycles, and typed-failure replay
87
+ - [Routing](https://weftui.dev/docs/how-to/add-routing): `@weftui/router` builds on this SSR + hydration model for full-page nested routing
84
88
  - [`Boundary.rpc` API reference](https://weftui.dev/docs/reference/core#boundaryrpc)
85
89
  - [`ServerTag` API reference](https://weftui.dev/docs/reference/core#servertag)
86
- - [examples/router-ssr](https://github.com/stefvw93/weft/tree/main/examples/router-ssr) a runnable shop with an SSR-replayed, refetchable live-stock `Boundary.rpc`
87
- - [examples/ssr-hydration](https://github.com/stefvw93/weft/tree/main/examples/ssr-hydration) SSR + hydration without server data loading
90
+ - [examples/router-ssr](https://github.com/stefvw93/weft/tree/main/examples/router-ssr): a runnable shop with an SSR-replayed, refetchable live-stock `Boundary.rpc`
91
+ - [examples/ssr-hydration](https://github.com/stefvw93/weft/tree/main/examples/ssr-hydration): SSR + hydration without server data loading
@@ -9,7 +9,9 @@ description: Render a pending indicator (e.g. a top progress bar) during a defer
9
9
 
10
10
  **Goal:** show a progress indicator while a [lazy route](https://weftui.dev/docs/how-to/split-routes-lazily) resolves its chunk and data, so a slow network is visible instead of feeling frozen.
11
11
 
12
- When you navigate to a route, the router is **deferred-commit**: it resolves the target branch's chunk (if the component is `Router.lazy`) **and the matched leaf's own component effect** including any data the leaf awaits in its body _before_ swapping the URL, keeping the previous page mounted for the whole window. That resolve window is exposed as a reactive signal, [`Router.navigating`](https://weftui.dev/docs/reference/router#routernavigating), that you read to render pending UI.
12
+ When you navigate to a route, the router is **deferred-commit**. It resolves the target branch's chunk (if the component is `Router.lazy`) **and the matched leaf's own component effect**, including any data the leaf awaits in its body. Only then does it swap the URL, keeping the previous page mounted for the whole window.
13
+
14
+ That resolve window is exposed as a reactive signal, [`Router.navigating`](https://weftui.dev/docs/reference/router#routernavigating), that you read to render pending UI.
13
15
 
14
16
  ```typescript
15
17
  import { Component, h } from "@weftui/core";
@@ -32,7 +34,7 @@ const Shell = Component.gen(function* () {
32
34
  });
33
35
  ```
34
36
 
35
- Thread the signal into a persistent layout (the outermost `Shell` is ideal, since it never re-renders across navigations), and style the pending class however you like a top bar, a cursor change, a dimmed outlet.
37
+ Thread the signal into a persistent layout (the outermost `Shell` is ideal, since it never re-renders across navigations), and style the pending class however you like: a top bar, a cursor change, a dimmed outlet.
36
38
 
37
39
  ## The signal
38
40
 
@@ -44,22 +46,22 @@ type NavState = { readonly _tag: "Idle" } | { readonly _tag: "Navigating"; reado
44
46
 
45
47
  Read it two ways, mirroring `Router.params` / `Router.paramsStream`:
46
48
 
47
- - `Router.navigating` the `Subscribable<NavState>` on the `Router` service.
48
- - `Router.navigatingStream` an `Effect` resolving that `Subscribable`, for use in a `Component.gen` body (as above).
49
+ - `Router.navigating`: the `Subscribable<NavState>` on the `Router` service.
50
+ - `Router.navigatingStream`: an `Effect` resolving that `Subscribable`, for use in a `Component.gen` body (as above).
49
51
 
50
52
  The `to` field on `Navigating` is the target URL, if you want to label _where_ the app is going.
51
53
 
52
54
  ## Behavior to expect
53
55
 
54
- - **Only navigations with real async work flip it.** A branch with no `Router.lazy` node and a leaf whose effect resolves synchronously (no async work, or a memoized revisit) commits in the same tick, and `navigating` stays `Idle` an entirely eager app never sees `Navigating`, and adding the reader costs nothing.
56
+ - **Only navigations with real async work flip it.** A branch with no `Router.lazy` node and a leaf whose effect resolves synchronously (no async work, or a memoized revisit) commits in the same tick, and `navigating` stays `Idle`. An entirely eager app never sees `Navigating`, and adding the reader costs nothing.
55
57
  - **Latest-wins.** Rapid successive navigations commit only the newest; a superseded navigation never resets the signal (the newer one owns it).
56
58
  - **Back/forward.** `popstate` into a route with async work also resolves before committing, so the indicator shows for browser back/forward too.
57
59
  - **Failure resets it.** A rejected chunk load or a failing leaf pre-run (a typed error such as `notFound()`, or a defect) resets `navigating` to `Idle` (it never sticks on), then surfaces through normal error/defect handling.
58
60
  - **Server renders `Idle`.** Server render is buffered, so `navigating` is a client-only concern; the server supplies a constant `Idle` so the same `Shell` type-checks and renders on both sides.
59
- - **No built-in anti-flash delay.** The signal flips as soon as an async window opens, so a borderline-fast navigation can flash the indicator briefly. If you want to only show it past a threshold, delay the reveal in CSS rather than in the signal e.g. `transition-delay: 200ms` on `.is-navigating` so genuinely fast navigations never flicker.
61
+ - **No built-in anti-flash delay.** The signal flips as soon as an async window opens, so a borderline-fast navigation can flash the indicator briefly. If you want to only show it past a threshold, delay the reveal in CSS rather than in the signal (e.g. `transition-delay: 200ms` on `.is-navigating`), so genuinely fast navigations never flicker.
60
62
 
61
63
  ## See also
62
64
 
63
65
  - [`Router.navigating` API reference](https://weftui.dev/docs/reference/router#routernavigating)
64
- - [Split Routes Lazily](https://weftui.dev/docs/how-to/split-routes-lazily) the `Router.lazy` deferred-commit navigation this reports on
65
- - [examples/router-ssr](https://github.com/stefvw93/weft/tree/main/examples/router-ssr) wires this exact progress bar in its `Shell` (`components/shell.ts`), with a `pending-navigation.browser.test.ts`
66
+ - [Split Routes Lazily](https://weftui.dev/docs/how-to/split-routes-lazily): the `Router.lazy` deferred-commit navigation this reports on
67
+ - [examples/router-ssr](https://github.com/stefvw93/weft/tree/main/examples/router-ssr): wires this exact progress bar in its `Shell` (`components/shell.ts`), with a `pending-navigation.browser.test.ts`
@@ -9,7 +9,7 @@ description: Code-split a route's component into its own chunk with Router.lazy,
9
9
 
10
10
  **Goal:** keep a heavy page's render code (and its dependencies) out of the initial bundle, loading it only when its route is actually rendered.
11
11
 
12
- Wrap the route's `component` in [`Router.lazy`](https://weftui.dev/docs/reference/router#routerlazy). The route **descriptor** (its segment and param schemas) stays eager so the matcher, `href`, and the server's dispatch API still see it statically only the component body is split into its own chunk.
12
+ Wrap the route's `component` in [`Router.lazy`](https://weftui.dev/docs/reference/router#routerlazy). The route **descriptor** (its segment and param schemas) stays eager, so the matcher, `href`, and the server's dispatch API still see it statically. Only the component body is split into its own chunk.
13
13
 
14
14
  ```typescript
15
15
  import { Router } from "@weftui/router";
@@ -21,42 +21,44 @@ Router.route("docs/:category/:slug", {
21
21
  });
22
22
  ```
23
23
 
24
- The chunk loads on the server during render and on the client on navigation; only the **matched branch's** chunks are ever fetched. `E`/`R` are preserved — a lazy route has the exact same channels as the same component declared eagerly, so an unmet service requirement is still a compile error at `Router.router(...)`.
24
+ The chunk loads on the server during render and on the client on navigation. Only the **matched branch's** chunks are ever fetched.
25
+
26
+ `E`/`R` are preserved: a lazy route has the exact same channels as the same component declared eagerly. An unmet service requirement is still a compile error at `Router.router(...)`.
25
27
 
26
28
  ## Make the split real
27
29
 
28
- `Router.lazy` only splits if the dynamic `import()` is the **only eager path** to the heavy module. Keep the `Router.route(…)` descriptor in an eagerly-imported file, and move the component implementation (and its heavy deps) into a separate module referenced _only_ through `Router.lazy(() => import("./impl"))`:
30
+ `Router.lazy` only splits if the dynamic `import()` is the **only eager path** to the heavy module. Keep the `Router.route(…)` descriptor in an eagerly-imported file. Move the component implementation (and its heavy deps) into a separate module referenced _only_ through `Router.lazy(() => import("./impl"))`:
29
31
 
30
32
  ```typescript
31
- // routes.ts eager, tiny: just the descriptor
33
+ // routes.ts: eager and tiny, just the descriptor
32
34
  export const docsRoute = Router.route("docs/:category/:slug", {
33
35
  path: { category: Schema.String, slug: Schema.String },
34
36
  component: Router.lazy(() => import("./doc-page-impl").then((m) => m.DocsPage)),
35
37
  });
36
38
 
37
- // doc-page-impl.ts heavy: pulled into its own chunk, never in the initial graph
39
+ // doc-page-impl.ts: heavy, pulled into its own chunk, never in the initial graph
38
40
  export const DocsPage = Component.gen(function* () {
39
41
  /* renderHast, code highlighting, … */
40
42
  });
41
43
  ```
42
44
 
43
- A descriptor file that still `import`s the impl statically gains nothing the bundler keeps it in the initial graph.
45
+ A descriptor file that still `import`s the impl statically gains nothing: the bundler keeps it in the initial graph.
44
46
 
45
47
  ## What you get for free
46
48
 
47
- - **Flash-free hydration.** On a directly-loaded lazy route, the client re-invokes the same slot, awaits the chunk, and adopts the server DOM in place the first production matches, so nothing is mutated.
48
- - **Blank-free navigation.** Client navigation is **deferred-commit**: the router resolves the target branch's chunk **and the matched leaf's own component effect** _before_ committing the URL, so the previous page stays mounted through both the fetch and any data the leaf awaits, and the swap is a single tick. See [Show Navigation Progress](https://weftui.dev/docs/how-to/show-navigation-progress) for the `Router.navigating` signal this exposes.
49
+ - **Flash-free hydration.** On a directly-loaded lazy route, the client re-invokes the same slot, awaits the chunk, and adopts the server DOM in place. The first production matches, so nothing is mutated.
50
+ - **Blank-free navigation.** Client navigation is **deferred-commit**: the router resolves the target branch's chunk **and the matched leaf's own component effect** _before_ committing the URL. The previous page stays mounted through the fetch and any data the leaf awaits, and the swap is a single tick. See [Show Navigation Progress](https://weftui.dev/docs/how-to/show-navigation-progress) for the `Router.navigating` signal this exposes.
49
51
  - **Synchronous revisits.** `Router.lazy` memoizes its load per slot, so a second visit to a loaded route commits immediately.
50
52
 
51
53
  ## Edge cases
52
54
 
53
- - **Lazy layouts.** A `Router.layout({ component: Router.lazy(...) })` splits too each lazy node in the matched branch is awaited; nodes outside it never load.
54
- - **Chunk-load failure is a defect.** If the `import()` rejects (offline, or a stale client requesting a chunk a new deploy removed), it dies as a defect and surfaces through normal defect handling it never hangs or silently 404s. The rejection is memoized, so the route keeps failing until a reload (the deploy-skew case).
55
- - **Not a lazy _subtree_.** Only the component is lazy; you cannot defer a whole `RouteNode` behind an `import()`, because the matcher needs every leaf's segment and param schema before anything loads.
55
+ - **Lazy layouts.** A `Router.layout({ component: Router.lazy(...) })` splits too. Each lazy node in the matched branch is awaited; nodes outside it never load.
56
+ - **Chunk-load failure is a defect.** If the `import()` rejects (offline, or a stale client requesting a chunk a new deploy removed), it dies as a defect and surfaces through normal defect handling. It never hangs or silently 404s. The rejection is memoized, so the route keeps failing until a reload (the deploy-skew case).
57
+ - **Not a lazy _subtree_.** Only the component is lazy. You cannot defer a whole `RouteNode` behind an `import()`; the matcher needs every leaf's segment and param schema before anything loads.
56
58
 
57
59
  ## See also
58
60
 
59
61
  - [`Router.lazy` API reference](https://weftui.dev/docs/reference/router#routerlazy)
60
- - [Show Navigation Progress](https://weftui.dev/docs/how-to/show-navigation-progress) the deferred-commit `Router.navigating` signal
61
- - [Add Routing](https://weftui.dev/docs/how-to/add-routing) authoring the route tree `Router.lazy` plugs into
62
- - [examples/router-ssr](https://github.com/stefvw93/weft/tree/main/examples/router-ssr) includes a `Router.lazy` page (`lazy-page.ts`) with a browser test
62
+ - [Show Navigation Progress](https://weftui.dev/docs/how-to/show-navigation-progress): the deferred-commit `Router.navigating` signal
63
+ - [Add Routing](https://weftui.dev/docs/how-to/add-routing): authoring the route tree `Router.lazy` plugs into
64
+ - [examples/router-ssr](https://github.com/stefvw93/weft/tree/main/examples/router-ssr): includes a `Router.lazy` page (`lazy-page.ts`) with a browser test