@weftui/core 0.29.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.
- package/dist/{index-4cTlhojA.d.ts → index-B-dPfhKZ.d.ts} +38 -20
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/types/index.d.ts +1 -1
- package/docs/explanation/boundaries-and-suspense.md +38 -8
- package/docs/explanation/combinator-api.md +17 -11
- package/docs/explanation/reactive-primitives.md +40 -13
- package/docs/explanation/rendering-model.md +14 -2
- package/docs/how-to/add-routing.md +299 -28
- package/docs/how-to/author-components.md +71 -105
- package/docs/how-to/compose-behavior-and-markup.md +141 -54
- package/docs/how-to/handle-forms.md +142 -15
- package/docs/how-to/load-async-data.md +110 -6
- package/docs/how-to/load-data-with-rpc.md +171 -18
- package/docs/how-to/provide-services.md +84 -1
- package/docs/how-to/render-keyed-lists.md +120 -14
- package/docs/how-to/render-on-the-server.md +113 -15
- package/docs/how-to/show-navigation-progress.md +120 -14
- package/docs/how-to/split-routes-lazily.md +106 -3
- package/docs/how-to/style-reactively.md +141 -13
- package/docs/how-to/use-element-refs.md +126 -8
- package/docs/reference/core.md +22 -2
- package/docs/reference/dom.md +43 -0
- package/docs/reference/router.md +2 -2
- package/docs/tutorial/01-your-first-app.md +43 -13
- package/docs/tutorial/02-reactivity.md +29 -30
- package/docs/tutorial/03-services-and-async.md +89 -38
- package/docs/tutorial/04-errors-and-server.md +41 -25
- package/package.json +1 -1
|
@@ -9,6 +9,8 @@ description: "Install Weft, build a component with the h namespace, and mount it
|
|
|
9
9
|
|
|
10
10
|
We assume you know [Effect](https://effect.website/docs/getting-started/introduction) fundamentals. Weft is Effect for the UI, so we will not re-explain `Effect.gen`, services, or streams from scratch.
|
|
11
11
|
|
|
12
|
+
Across this tutorial you build one app: a counter. This step renders its static shell.
|
|
13
|
+
|
|
12
14
|
## Install
|
|
13
15
|
|
|
14
16
|
```bash
|
|
@@ -17,33 +19,61 @@ npm install @weftui/core @weftui/dom effect@beta
|
|
|
17
19
|
|
|
18
20
|
Weft tracks Effect 4's beta line. This release is built and tested against `effect@4.0.0-beta.98`; the peer range accepts newer 4.0 betas, which may contain upstream breaking changes.
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
## Build a component
|
|
22
|
+
## Build and mount it
|
|
23
23
|
|
|
24
|
-
A **component is a plain function you call**. There is no JSX and no `<Component/>` deferral.
|
|
24
|
+
A **component is a plain function you call**. There is no JSX and no `<Component/>` deferral. `App()` returns a `Node`, and the `h` namespace builds one: every property (`h.div`, `h.h1`, `h.button`, …) is a builder for that HTML tag, taking optional props and children.
|
|
25
25
|
|
|
26
26
|
```typescript
|
|
27
|
+
// src/app.ts
|
|
27
28
|
import { h } from "@weftui/core";
|
|
29
|
+
|
|
30
|
+
export function App() {
|
|
31
|
+
return h.div({ class: "app" }, [
|
|
32
|
+
h.h1("Weft Counter"),
|
|
33
|
+
h.p({ class: "count" }, "Count: 0"),
|
|
34
|
+
h.div({ class: "controls" }, [
|
|
35
|
+
h.button({ type: "button" }, "−"),
|
|
36
|
+
h.button({ type: "button" }, "+"),
|
|
37
|
+
]),
|
|
38
|
+
]);
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
// src/main.ts
|
|
28
44
|
import { WeftApp } from "@weftui/dom/client";
|
|
29
45
|
import { Effect } from "effect";
|
|
46
|
+
import { App } from "./app";
|
|
30
47
|
|
|
31
|
-
|
|
32
|
-
return h.div({ class: "app" }, [h.h1("Hello, Weft"), h.p("A minimal app.")]);
|
|
33
|
-
}
|
|
48
|
+
const root = document.getElementById("root")!;
|
|
34
49
|
|
|
35
50
|
const app = WeftApp.make();
|
|
36
|
-
void Effect.runPromise(WeftApp.mount(app, App(),
|
|
51
|
+
void Effect.runPromise(WeftApp.mount(app, App(), root));
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```html
|
|
55
|
+
<!-- index.html -->
|
|
56
|
+
<!doctype html>
|
|
57
|
+
<html lang="en">
|
|
58
|
+
<head>
|
|
59
|
+
<meta charset="UTF-8" />
|
|
60
|
+
<title>Weft counter</title>
|
|
61
|
+
</head>
|
|
62
|
+
<body>
|
|
63
|
+
<div id="root"></div>
|
|
64
|
+
<script type="module" src="/src/main.ts"></script>
|
|
65
|
+
</body>
|
|
66
|
+
</html>
|
|
37
67
|
```
|
|
38
68
|
|
|
39
|
-
|
|
69
|
+
Run it with `vite` (or any dev server that serves ES modules) and you get a heading, a static count, and two inert buttons. The buttons don't do anything yet: that's next.
|
|
40
70
|
|
|
41
71
|
## What just happened
|
|
42
72
|
|
|
43
|
-
- `App()` returns a **`Node<never, never
|
|
44
|
-
- `WeftApp.make()` creates a Weft app synchronously, with no layer to build yet. `WeftApp.mount(app, node,
|
|
45
|
-
-
|
|
73
|
+
- `App()` returns a **`Node<never, never>`**, an `Effect` that resolves to an element descriptor, not a DOM node yet. `E` and `R` are `never` because this component neither fails nor needs a service. As your app grows, those channels accumulate what it can fail with and what it depends on: see [The Rendering Model](https://weftui.dev/docs/explanation/rendering-model).
|
|
74
|
+
- `WeftApp.make()` creates a Weft app synchronously, with no layer to build yet. `WeftApp.mount(app, node, root)` renders `node` into `root`, building real DOM. It returns `Effect<RootHandle, …>` with `R = never`, so a bare `Effect.runPromise` runs it. You'll give `WeftApp.make` a `Layer` once components need services: see [Services and Async](https://weftui.dev/docs/tutorial/03-services-and-async).
|
|
75
|
+
- `App` runs **once**. Nothing re-invokes it, because there's no state yet.
|
|
46
76
|
|
|
47
77
|
## Next
|
|
48
78
|
|
|
49
|
-
- [Reactivity →](https://weftui.dev/docs/tutorial/02-reactivity):
|
|
79
|
+
- [Reactivity →](https://weftui.dev/docs/tutorial/02-reactivity): wire up the counter with `SubscriptionRef` and streams
|
|
@@ -7,54 +7,53 @@ description: Add component-local state with SubscriptionRef and weave its stream
|
|
|
7
7
|
|
|
8
8
|
# Reactivity
|
|
9
9
|
|
|
10
|
-
[Previously](https://weftui.dev/docs/tutorial/01-your-first-app)
|
|
10
|
+
[Previously](https://weftui.dev/docs/tutorial/01-your-first-app) you mounted a static counter shell. Now wire it up. This is the defining move in Weft: **weave a stream through the tree, and only that point updates.**
|
|
11
11
|
|
|
12
|
-
##
|
|
12
|
+
## Wire up the counter
|
|
13
13
|
|
|
14
|
-
Use Effect's `SubscriptionRef` for component-local state. `SubscriptionRef.changes(ref)` returns a `Stream` that emits the current value and then every update. Pass that stream as a child
|
|
14
|
+
Use Effect's `SubscriptionRef` for component-local state. `SubscriptionRef.changes(ref)` returns a `Stream` that emits the current value and then every update. Pass that stream (or a derived stream) as a child and the DOM at that spot becomes live. Replace `src/app.ts`:
|
|
15
15
|
|
|
16
16
|
```typescript
|
|
17
|
+
// src/app.ts
|
|
17
18
|
import { h } from "@weftui/core";
|
|
18
|
-
import {
|
|
19
|
-
import { Effect, SubscriptionRef } from "effect";
|
|
19
|
+
import { Effect, Stream, SubscriptionRef } from "effect";
|
|
20
20
|
|
|
21
|
-
const
|
|
21
|
+
export const App = () =>
|
|
22
22
|
Effect.gen(function* () {
|
|
23
23
|
const count = yield* SubscriptionRef.make(0);
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
h.
|
|
28
|
-
h.
|
|
24
|
+
const label = Stream.map(SubscriptionRef.changes(count), (n) => `Count: ${n}`);
|
|
25
|
+
|
|
26
|
+
return yield* h.div({ class: "app" }, [
|
|
27
|
+
h.h1("Weft Counter"),
|
|
28
|
+
h.p({ class: "count" }, [label]),
|
|
29
|
+
h.div({ class: "controls" }, [
|
|
30
|
+
h.button(
|
|
31
|
+
{ type: "button", onclick: () => SubscriptionRef.update(count, (n) => n - 1) },
|
|
32
|
+
"−",
|
|
33
|
+
),
|
|
34
|
+
h.button(
|
|
35
|
+
{ type: "button", onclick: () => SubscriptionRef.update(count, (n) => n + 1) },
|
|
36
|
+
"+",
|
|
37
|
+
),
|
|
38
|
+
]),
|
|
29
39
|
]);
|
|
30
40
|
});
|
|
31
|
-
|
|
32
|
-
const app = WeftApp.make();
|
|
33
|
-
void Effect.runPromise(WeftApp.mount(app, Counter(), document.getElementById("root")!));
|
|
34
41
|
```
|
|
35
42
|
|
|
36
|
-
`
|
|
37
|
-
|
|
38
|
-
## The key idea: the body runs once
|
|
39
|
-
|
|
40
|
-
The `Counter` function runs **exactly once**. It creates the ref, builds the tree, and returns. After that, nothing re-invokes it. The only thing that changes the DOM is the `SubscriptionRef.changes(count)` stream woven into the `h.span`.
|
|
43
|
+
`main.ts` and `index.html` don't change. Reload and the buttons work.
|
|
41
44
|
|
|
42
|
-
|
|
45
|
+
## Why this works
|
|
43
46
|
|
|
44
|
-
|
|
47
|
+
`App`'s body runs **exactly once**: it creates the ref, builds the tree, and returns. Nothing re-invokes it afterward. The only thing that changes the DOM is the `label` stream woven into `h.p`.
|
|
45
48
|
|
|
46
|
-
|
|
49
|
+
Click `+` and `SubscriptionRef.update` pushes a new value, `label` emits `"Count: 1"`, and the renderer patches _just that paragraph's text_ in place. No diff, no re-render, no sibling touched.
|
|
47
50
|
|
|
48
|
-
|
|
51
|
+
`label` also shows **deriving values**: because `SubscriptionRef.changes(count)` is a `Stream`, you shape reactive text with ordinary stream operators (`Stream.map` here) instead of a templating syntax. Anywhere you'd compute a derived value, map the stream.
|
|
49
52
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
```typescript
|
|
53
|
-
h.span([Stream.map(SubscriptionRef.changes(count), (n) => `Count: ${n}`)]);
|
|
54
|
-
```
|
|
53
|
+
> **Note.** A stream-shaped child or prop is reactive; a static value (`"Hello"`, `5`) is not and never changes. `h.h1("Weft Counter")` above is static for exactly that reason. The rule is uniform across the whole tree.
|
|
55
54
|
|
|
56
|
-
|
|
55
|
+
The full model is [The Rendering Model](https://weftui.dev/docs/explanation/rendering-model); the vocabulary of stream-shaped values is [Reactive Primitives](https://weftui.dev/docs/explanation/reactive-primitives).
|
|
57
56
|
|
|
58
57
|
## Next
|
|
59
58
|
|
|
60
|
-
- [Services and Async →](https://weftui.dev/docs/tutorial/03-services-and-async):
|
|
59
|
+
- [Services and Async →](https://weftui.dev/docs/tutorial/03-services-and-async): read services from a button handler and load data asynchronously
|
|
@@ -7,15 +7,13 @@ description: Give handlers access to services from the environment, and render a
|
|
|
7
7
|
|
|
8
8
|
# Services and Async
|
|
9
9
|
|
|
10
|
-
[So far](https://weftui.dev/docs/tutorial/02-reactivity)
|
|
10
|
+
[So far](https://weftui.dev/docs/tutorial/02-reactivity) the counter's state has been self-contained. Real apps talk to services and wait on async work. Both fall out of the same fact (a `Node` is an `Effect`), so both use plain Effect. This step adds a logging service to the buttons and an async fact panel below the counter.
|
|
11
11
|
|
|
12
12
|
## Handlers that use services
|
|
13
13
|
|
|
14
14
|
An event handler can **return an Effect**. That Effect runs in the app's environment, so it can read any service the app's layer provides:
|
|
15
15
|
|
|
16
16
|
```typescript
|
|
17
|
-
import { h } from "@weftui/core";
|
|
18
|
-
import { WeftApp } from "@weftui/dom/client";
|
|
19
17
|
import { Context, Effect, Layer } from "effect";
|
|
20
18
|
|
|
21
19
|
class Logger extends Context.Service<Logger, { log: (message: string) => Effect.Effect<void> }>()(
|
|
@@ -23,60 +21,113 @@ class Logger extends Context.Service<Logger, { log: (message: string) => Effect.
|
|
|
23
21
|
) {}
|
|
24
22
|
|
|
25
23
|
const LoggerLive = Layer.succeed(Logger, {
|
|
26
|
-
log: (message) => Effect.
|
|
24
|
+
log: (message) => Effect.log(message),
|
|
27
25
|
});
|
|
26
|
+
```
|
|
28
27
|
|
|
29
|
-
|
|
30
|
-
h.button(
|
|
31
|
-
{
|
|
32
|
-
onclick: () =>
|
|
33
|
-
Effect.gen(function* () {
|
|
34
|
-
const logger = yield* Logger;
|
|
35
|
-
yield* logger.log("Button clicked");
|
|
36
|
-
}),
|
|
37
|
-
},
|
|
38
|
-
"Log",
|
|
39
|
-
);
|
|
28
|
+
Wire it into the counter's `step` handler, so every click logs before updating state:
|
|
40
29
|
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
30
|
+
```typescript
|
|
31
|
+
const step = (delta: number) =>
|
|
32
|
+
Effect.gen(function* () {
|
|
33
|
+
const logger = yield* Logger;
|
|
34
|
+
yield* SubscriptionRef.update(count, (n) => n + delta);
|
|
35
|
+
yield* logger.log(`count changed by ${delta}`);
|
|
36
|
+
});
|
|
44
37
|
```
|
|
45
38
|
|
|
46
|
-
`Logger` entered the tree's requirement channel the moment `
|
|
47
|
-
|
|
48
|
-
Services come exclusively from the app's layer: an `Effect.provide` wrapped around the `mount` call does **not** reach components or handlers. This is Weft's entire dependency-injection story; it is just Effect's. The deeper treatment is [Services and Context](https://weftui.dev/docs/explanation/services-and-context).
|
|
39
|
+
`Logger` entered the tree's requirement channel the moment `step` read it. You'll discharge it **once**, by passing `LoggerLive` to `WeftApp.make`. Provide too little and it's a compile error. Services come exclusively from the app's layer: an `Effect.provide` wrapped around the `mount` call does **not** reach components or handlers. This is Weft's entire dependency-injection story; it's just Effect's. The deeper treatment is [Services and Context](https://weftui.dev/docs/explanation/services-and-context).
|
|
49
40
|
|
|
50
41
|
## Async loading states
|
|
51
42
|
|
|
52
43
|
A component can return a **`Stream<Node>`** to show different content over time. Sequence a loading placeholder before the resolved content with `Stream.concat`:
|
|
53
44
|
|
|
54
45
|
```typescript
|
|
46
|
+
const fetchFact = (n: number) =>
|
|
47
|
+
Effect.gen(function* () {
|
|
48
|
+
yield* Effect.sleep("800 millis");
|
|
49
|
+
return `${n} is ${n % 2 === 0 ? "even" : "odd"}.`;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const NumberFact = ({ n }: { n: number }) =>
|
|
53
|
+
Stream.concat(
|
|
54
|
+
Stream.make(h.p({ class: "fact" }, "Loading a fact…")),
|
|
55
|
+
Stream.fromEffect(Effect.map(fetchFact(n), (fact) => h.p({ class: "fact" }, fact))),
|
|
56
|
+
);
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The stream emits the loading node first, then the resolved node. The renderer swaps the DOM in place on the second emission. This is the raw mechanism; to coordinate _several_ async regions with one fallback, reach for [`Boundary.suspend`](https://weftui.dev/docs/explanation/boundaries-and-suspense).
|
|
60
|
+
|
|
61
|
+
## Put it together
|
|
62
|
+
|
|
63
|
+
Replace `src/app.ts`, adding both pieces as part of the same tree:
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
// src/app.ts
|
|
55
67
|
import { h } from "@weftui/core";
|
|
56
|
-
import {
|
|
57
|
-
|
|
68
|
+
import { Context, Effect, Layer, Stream, SubscriptionRef } from "effect";
|
|
69
|
+
|
|
70
|
+
export class Logger extends Context.Service<
|
|
71
|
+
Logger,
|
|
72
|
+
{ log: (message: string) => Effect.Effect<void> }
|
|
73
|
+
>()("Logger") {}
|
|
74
|
+
|
|
75
|
+
export const LoggerLive = Layer.succeed(Logger, {
|
|
76
|
+
log: (message) => Effect.log(message),
|
|
77
|
+
});
|
|
58
78
|
|
|
59
|
-
const
|
|
79
|
+
const fetchFact = (n: number) =>
|
|
80
|
+
Effect.gen(function* () {
|
|
81
|
+
yield* Effect.sleep("800 millis");
|
|
82
|
+
return `${n} is ${n % 2 === 0 ? "even" : "odd"}.`;
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const NumberFact = ({ n }: { n: number }) =>
|
|
60
86
|
Stream.concat(
|
|
61
|
-
Stream.make(h.
|
|
62
|
-
Stream.fromEffect(
|
|
63
|
-
Effect.gen(function* () {
|
|
64
|
-
yield* Effect.sleep("1 second");
|
|
65
|
-
return yield* h.span(`Hello, ${name}!`);
|
|
66
|
-
}),
|
|
67
|
-
),
|
|
87
|
+
Stream.make(h.p({ class: "fact" }, "Loading a fact…")),
|
|
88
|
+
Stream.fromEffect(Effect.map(fetchFact(n), (fact) => h.p({ class: "fact" }, fact))),
|
|
68
89
|
);
|
|
69
90
|
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
);
|
|
91
|
+
export const App = () =>
|
|
92
|
+
Effect.gen(function* () {
|
|
93
|
+
const count = yield* SubscriptionRef.make(0);
|
|
94
|
+
const label = Stream.map(SubscriptionRef.changes(count), (n) => `Count: ${n}`);
|
|
95
|
+
|
|
96
|
+
const step = (delta: number) =>
|
|
97
|
+
Effect.gen(function* () {
|
|
98
|
+
const logger = yield* Logger;
|
|
99
|
+
yield* SubscriptionRef.update(count, (n) => n + delta);
|
|
100
|
+
yield* logger.log(`count changed by ${delta}`);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return yield* h.div({ class: "app" }, [
|
|
104
|
+
h.h1("Weft Counter"),
|
|
105
|
+
h.p({ class: "count" }, [label]),
|
|
106
|
+
h.div({ class: "controls" }, [
|
|
107
|
+
h.button({ type: "button", onclick: () => step(-1) }, "−"),
|
|
108
|
+
h.button({ type: "button", onclick: () => step(1) }, "+"),
|
|
109
|
+
]),
|
|
110
|
+
NumberFact({ n: 3 }),
|
|
111
|
+
]);
|
|
112
|
+
});
|
|
74
113
|
```
|
|
75
114
|
|
|
76
|
-
|
|
115
|
+
Give the app the layer in `src/main.ts`:
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
// src/main.ts
|
|
119
|
+
import { WeftApp } from "@weftui/dom/client";
|
|
120
|
+
import { Effect } from "effect";
|
|
121
|
+
import { App, LoggerLive } from "./app";
|
|
122
|
+
|
|
123
|
+
const root = document.getElementById("root")!;
|
|
124
|
+
|
|
125
|
+
const app = WeftApp.make(LoggerLive);
|
|
126
|
+
void Effect.runPromise(WeftApp.mount(app, App(), root));
|
|
127
|
+
```
|
|
77
128
|
|
|
78
|
-
|
|
129
|
+
Reload: the fact panel shows "Loading a fact…" then swaps in, and every click logs to the console.
|
|
79
130
|
|
|
80
131
|
## Next
|
|
81
132
|
|
|
82
|
-
- [Errors and Server Rendering →](https://weftui.dev/docs/tutorial/04-errors-and-server): catch failures with
|
|
133
|
+
- [Errors and Server Rendering →](https://weftui.dev/docs/tutorial/04-errors-and-server): catch the fact panel's failures with a boundary and render the whole app on the server
|
|
@@ -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
|
|
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
|
|
20
|
+
class FactError extends Data.TaggedError("FactError")<{ n: number }> {}
|
|
21
21
|
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
Effect.
|
|
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
|
-
|
|
30
|
+
Wrap the fact panel where it's placed in `App`:
|
|
29
31
|
|
|
30
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
52
|
-
void Effect.runPromise(WeftApp.hydrate(app, App(), document.getElementById("root")!));
|
|
53
|
-
```
|
|
70
|
+
const root = document.getElementById("root")!;
|
|
54
71
|
|
|
55
|
-
|
|
72
|
+
const app = WeftApp.make(LoggerLive);
|
|
73
|
+
void Effect.runPromise(WeftApp.hydrate(app, App(), root));
|
|
74
|
+
```
|
|
56
75
|
|
|
57
|
-
|
|
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
|
-
|
|
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
|
|
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)
|