@weftui/router 0.30.0 → 0.31.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.
@@ -7,63 +7,79 @@ description: Catch rendering-path failures with Boundary, then render on the ser
7
7
 
8
8
  # Errors and Server Rendering
9
9
 
10
- The final step. [We can now](https://weftui.dev/docs/tutorial/03-services-and-async) use services and async. Here we handle what happens when async work **fails**, and how the same tree renders on the server.
10
+ The final step. [We can now](https://weftui.dev/docs/tutorial/03-services-and-async) use services and async. Here we make the fact panel's fetch fail on purpose, catch it with a boundary, and render the whole counter on the server.
11
11
 
12
12
  ## Error boundaries
13
13
 
14
- A component's failures accumulate on its `E` channel. Wrap a subtree in a `Boundary.*` variant to intercept them and render a fallback instead of failing the mount:
14
+ A component's failures accumulate on its `E` channel. Wrap a subtree in a `Boundary.*` variant to intercept them and render a fallback instead of failing the mount. In `src/app.ts`, make `fetchFact` fail for `n === 3`, the value the counter's fact panel actually requests:
15
15
 
16
16
  ```typescript
17
17
  import { Boundary, h } from "@weftui/core";
18
18
  import { Data, Effect } from "effect";
19
19
 
20
- class ApiError extends Data.TaggedError("ApiError")<{ status: number }> {}
20
+ class FactError extends Data.TaggedError("FactError")<{ n: number }> {}
21
21
 
22
- const SafeWidget = () =>
23
- Boundary.catch({ fallback: (e) => h.div({ class: "error" }, `Request failed: ${e.status}`) }, [
24
- Effect.fail(new ApiError({ status: 503 })),
25
- ]);
22
+ const fetchFact = (n: number): Effect.Effect<string, FactError> =>
23
+ Effect.gen(function* () {
24
+ yield* Effect.sleep("800 millis");
25
+ if (n === 3) return yield* Effect.fail(new FactError({ n }));
26
+ return `${n} is ${n % 2 === 0 ? "even" : "odd"}.`;
27
+ });
26
28
  ```
27
29
 
28
- There are six failure-catch variants, mirroring Effect's own error operators: `catch`, `catchCause`, `catchTag`, `catchTags`, `catchFilter`, `catchIf`. A failure that a boundary does not match re-raises to the **nearest enclosing** boundary. If none catches it, the mount fails.
30
+ Wrap the fact panel where it's placed in `App`:
29
31
 
30
- The conceptual model (and why the boundary's type reflects exactly which failures are handled) is [Boundaries and Suspense](https://weftui.dev/docs/explanation/boundaries-and-suspense).
32
+ ```typescript
33
+ Boundary.catch(
34
+ { fallback: (e) => h.p({ class: "error" }, `Couldn't load a fact about ${e.n}.`) },
35
+ [NumberFact({ n: 3 })],
36
+ ),
37
+ ```
38
+
39
+ There are six failure-catch variants, mirroring Effect's own error operators: `catch`, `catchCause`, `catchTag`, `catchTags`, `catchFilter`, `catchIf`. `Boundary.catch` here fully consumes `FactError` from the subtree's `E`; the app's aggregate `E` stays `never`. A failure a boundary doesn't match re-raises to the **nearest enclosing** boundary; if none catches it, the mount fails. The conceptual model (and why the boundary's type reflects exactly which failures are handled) is [Boundaries and Suspense](https://weftui.dev/docs/explanation/boundaries-and-suspense).
40
+
41
+ Reload and the fact panel shows "Couldn't load a fact about 3." instead of hanging or crashing the mount.
31
42
 
32
43
  ## Render on the server
33
44
 
34
- The same component tree renders to HTML on the server and **hydrates in place** on the client: no re-render, no flash. The server produces markup (plus inline data). `hydrate` adopts that existing DOM and resumes reactivity:
45
+ The same component tree renders to HTML on the server and **hydrates in place** on the client: no re-render, no flash. `hydrate` adopts the server's existing DOM and resumes reactivity. Split `main.ts` into two entries that both import the same side-effect-free `App`:
35
46
 
36
47
  ```typescript
37
- // server entry
48
+ // src/entry-server.ts
49
+ import { AppRpcClientTag } from "@weftui/core";
38
50
  import { renderToStringHydratable } from "@weftui/dom/server";
39
- import { Effect } from "effect";
51
+ import { Effect, Layer } from "effect";
40
52
  import { App } from "./app";
41
53
 
42
- export const render = () => Effect.runPromise(renderToStringHydratable(App()));
54
+ // This tree has no `Boundary.rpc`, but the SSR renderer always requires an
55
+ // AppRpcClientTag in context, so discharge it with a no-op.
56
+ const NoRpc = Layer.succeed(AppRpcClientTag, {
57
+ call: () => Effect.die(new Error("no rpc in this app")),
58
+ });
59
+
60
+ export const render = (): Promise<string> =>
61
+ Effect.runPromise(Effect.provide(renderToStringHydratable(App()), NoRpc));
43
62
  ```
44
63
 
45
64
  ```typescript
46
- // client entry
65
+ // src/entry-client.ts
47
66
  import { WeftApp } from "@weftui/dom/client";
48
67
  import { Effect } from "effect";
49
- import { App } from "./app";
68
+ import { App, LoggerLive } from "./app";
50
69
 
51
- const app = WeftApp.make();
52
- void Effect.runPromise(WeftApp.hydrate(app, App(), document.getElementById("root")!));
53
- ```
70
+ const root = document.getElementById("root")!;
54
71
 
55
- The same side-effect-free `App` is imported by both entries.
72
+ const app = WeftApp.make(LoggerLive);
73
+ void Effect.runPromise(WeftApp.hydrate(app, App(), root));
74
+ ```
56
75
 
57
- For server-resolved data that replays into the client without a second request, `Boundary.rpc` extends this model:
76
+ Splice `render()`'s HTML into your server template's `#root`, and point `index.html`'s script tag at `entry-client.ts` instead of `main.ts`. `AppRpcClientTag` and the `NoRpc` no-op only matter here because `renderToStringHydratable` requires that seam unconditionally; a tree using [`Boundary.rpc`](https://weftui.dev/docs/how-to/load-data-with-rpc) would provide a real one instead, typically via `@weftui/router`'s `RouterServer`.
58
77
 
59
- 1. Resolve an rpc on the server.
60
- 2. Serialize its result into the HTML.
61
- 3. Replay it on hydrate.
62
- 4. Keep the region live for refetch.
78
+ For server-resolved data that replays into the client without a second request, `Boundary.rpc` extends this model: resolve an rpc on the server, serialize its result into the HTML, replay it on hydrate, and keep the region live for refetch.
63
79
 
64
80
  ## You're done
65
81
 
66
- You have built up every core idea: components and `h`, reactive state and streams, services and async, boundaries and SSR. Where to go next depends on what you are doing:
82
+ You've built up every core idea: components and `h`, reactive state and streams, services and async, boundaries and SSR, all in one counter. Where to go next depends on what you're doing:
67
83
 
68
84
  - **Understand the model** → [The Rendering Model](https://weftui.dev/docs/explanation/rendering-model), [The Combinator API](https://weftui.dev/docs/explanation/combinator-api), [Reactive Primitives](https://weftui.dev/docs/explanation/reactive-primitives)
69
85
  - **Get a task done** → [Author Components](https://weftui.dev/docs/how-to/author-components), [Render on the Server](https://weftui.dev/docs/how-to/render-on-the-server), [Load Data with RPC](https://weftui.dev/docs/how-to/load-data-with-rpc), [Add Routing](https://weftui.dev/docs/how-to/add-routing)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weftui/router",
3
- "version": "0.30.0",
3
+ "version": "0.31.0",
4
4
  "description": "Universal nested router for Weft: one route tree, server and client, with type-safe params and href",
5
5
  "keywords": [
6
6
  "effect",
@@ -44,8 +44,8 @@
44
44
  "access": "public"
45
45
  },
46
46
  "dependencies": {
47
- "@weftui/core": "0.30.0",
48
- "@weftui/dom": "0.30.0"
47
+ "@weftui/core": "0.31.0",
48
+ "@weftui/dom": "0.31.0"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@types/jsdom": "^28.0.3",