@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.
@@ -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
- `@weftui/core` gives you the element builders and combinators; `@weftui/dom` renders them (its `./client` entry mounts in the browser). `effect` is the peer everything is built on.
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. It returns a `Node`, which is just an `Effect` that produces a DOM node:
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
- function App() {
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(), document.getElementById("root")!));
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
- The `h` namespace is the entry point: every property (`h.div`, `h.h1`, `h.button`, …) is a builder for that HTML tag. A builder takes optional props and children, and returns a `Node`.
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>`**. The two type parameters are the error channel (`E`) and the requirement channel (`R`), both `never` here 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. That is the whole point of Weft's types: see [The Rendering Model](https://weftui.dev/docs/explanation/rendering-model).
44
- - `WeftApp.make()` creates a Weft app synchronously, with no layer to build yet. `WeftApp.mount(app, node, target)` renders the node into `target`, building real DOM and starting any reactive streams. It returns `Effect<RootHandle, …>` with `R = never`, so a bare `Effect.runPromise` runs it with no `Effect.provide` needed. You will give `WeftApp.make` a `Layer` once components need services: see [Services and Async](https://weftui.dev/docs/tutorial/03-services-and-async).
45
- - The component function runs **once**. Nothing here re-runs on a timer or a state change, because there is no state yet. That comes next.
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): make the UI change over time with `SubscriptionRef` and streams
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) we mounted a static component. Now we make it change over time. This is the defining move in Weft: **weave a stream through the tree, and only that point updates.**
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
- ## Local state with `SubscriptionRef`
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 or prop and the DOM at that spot becomes live:
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 { WeftApp } from "@weftui/dom/client";
19
- import { Effect, SubscriptionRef } from "effect";
19
+ import { Effect, Stream, SubscriptionRef } from "effect";
20
20
 
21
- const Counter = () =>
21
+ export const App = () =>
22
22
  Effect.gen(function* () {
23
23
  const count = yield* SubscriptionRef.make(0);
24
-
25
- return yield* h.div([
26
- h.span([SubscriptionRef.changes(count)]),
27
- h.button({ onclick: () => SubscriptionRef.update(count, (n) => n + 1) }, "+"),
28
- h.button({ onclick: () => SubscriptionRef.update(count, (n) => n - 1) }, "-"),
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
- `Effect.gen` lets you `yield*` the `SubscriptionRef` to set up state **before** building the tree. Because a `Node` is an `Effect`, the component body is an ordinary generator: no hooks, no dependency arrays.
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
- When you click `+`, `SubscriptionRef.update` pushes a new value, the stream emits, and the renderer patches _just that span's text_ in place. No diff, no re-render, no sibling touched.
45
+ ## Why this works
43
46
 
44
- This is what "streams are the weft" means in practice: reactivity is local to exactly where you thread a stream. Everything else is static. 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).
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
- > **Note.** In `[SubscriptionRef.changes(count)]` the stream is passed as a child array. Static values (`"Hello"`, `5`) work in the same position and simply never change. The rule is uniform: a plain value is static, a stream-shaped value is reactive.
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
- ## Deriving values
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
- Because `SubscriptionRef.changes(count)` returns a `Stream`, you shape reactive text with ordinary stream operators:
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
- Anywhere you would compute a derived value, map the stream instead. The derivation stays reactive.
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): pull dependencies from the environment and render async loading states
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) our 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.
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.sync(() => console.log(message)),
24
+ log: (message) => Effect.log(message),
27
25
  });
26
+ ```
28
27
 
29
- const LogButton = () =>
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
- // Give the app the layer: every handler in every root can now read Logger.
42
- const app = WeftApp.make(LoggerLive);
43
- void Effect.runPromise(WeftApp.mount(app, LogButton(), document.getElementById("root")!));
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 `LogButton` read it. You discharged it **once**, by passing `LoggerLive` to `WeftApp.make`. Provide too little and it is a compile error. The type of `app` (and so of `WeftApp.mount(app, LogButton(), …)`) names exactly which service is missing.
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 { WeftApp } from "@weftui/dom/client";
57
- import { Effect, Stream } from "effect";
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 AsyncGreeting = ({ name }: { name: string }) =>
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.span("Loading…")),
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 app = WeftApp.make();
71
- void Effect.runPromise(
72
- WeftApp.mount(app, AsyncGreeting({ name: "World" }), document.getElementById("root")!),
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
- The stream emits the loading node first, then the resolved node. The renderer swaps the DOM in place on the second emission.
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
- This is the raw mechanism. To coordinate _several_ async regions with a single fallback, reach for [`Boundary.suspend`](https://weftui.dev/docs/explanation/boundaries-and-suspense), which you will meet in the next step.
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 boundaries and render on the server
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 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/core",
3
- "version": "0.29.0",
3
+ "version": "0.31.0",
4
4
  "description": "Element builders and combinators for Weft: reactive UI, woven from Effect",
5
5
  "keywords": [
6
6
  "combinators",