@weftui/dom 0.27.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -9
- package/dist/client/index.d.ts +159 -146
- package/dist/client/index.js +1 -1
- package/dist/{data-Bk7mnjdd.d.ts → data-C88W1AwU.d.ts} +4 -5
- package/dist/index.d.ts +1 -1
- package/dist/rolldown-runtime-DK3Fl9T5.js +1 -0
- package/dist/server/index.d.ts +0 -1
- package/docs/explanation/boundaries-and-suspense.md +2 -2
- package/docs/explanation/reactive-primitives.md +1 -1
- package/docs/explanation/rendering-model.md +2 -2
- package/docs/explanation/services-and-context.md +13 -17
- package/docs/how-to/add-routing.md +7 -7
- package/docs/how-to/author-components.md +7 -5
- package/docs/how-to/load-data-with-rpc.md +4 -2
- package/docs/how-to/provide-services.md +52 -74
- package/docs/how-to/render-on-the-server.md +4 -3
- package/docs/index.md +2 -2
- package/docs/reference/dom.md +166 -45
- package/docs/reference/router.md +5 -5
- package/docs/tutorial/01-your-first-app.md +7 -4
- package/docs/tutorial/02-reactivity.md +3 -2
- package/docs/tutorial/03-services-and-async.md +12 -10
- package/docs/tutorial/04-errors-and-server.md +3 -2
- package/package.json +10 -9
|
@@ -46,9 +46,9 @@ This also fixes the update _shape_. Because the structure is fixed, an update is
|
|
|
46
46
|
The same component tree renders on the server and the client:
|
|
47
47
|
|
|
48
48
|
- On the **server**, the tree renders to an HTML string (or a streaming response) via `@weftui/dom/server`. The _hydratable_ renderers additionally emit the inline data each reactive region needs to resume.
|
|
49
|
-
- On the **client**, `hydrate
|
|
49
|
+
- On the **client**, `WeftApp.hydrate` walks that server-rendered DOM and **adopts it in place** — it wires up reactivity and event handlers on the existing nodes rather than re-rendering. The first client production matches the adopted DOM exactly, so nothing is mutated and there is no flash.
|
|
50
50
|
|
|
51
|
-
Because the same `Node<E, R>` describes both passes, there is nothing to keep in sync: the server output and the client's first render are the _same tree_ run in two environments. Services flow from the
|
|
51
|
+
Because the same `Node<E, R>` describes both passes, there is nothing to keep in sync: the server output and the client's first render are the _same tree_ run in two environments. Services flow from the app layer (or the router's render-time context) through the tree to wherever a component reads them, on both sides. The mechanics of the two-sided render live in [Render on the Server](https://weftui.dev/docs/how-to/render-on-the-server); the service flow is [Services and Context](https://weftui.dev/docs/explanation/services-and-context).
|
|
52
52
|
|
|
53
53
|
## Why this matters
|
|
54
54
|
|
|
@@ -15,37 +15,33 @@ When a component does `yield* ThemeService`, `ThemeService` enters that node's r
|
|
|
15
15
|
|
|
16
16
|
```typescript
|
|
17
17
|
import { Effect } from "effect";
|
|
18
|
-
import {
|
|
18
|
+
import { WeftApp } from "@weftui/dom/client";
|
|
19
19
|
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
Effect.provide(ThemeServiceLive),
|
|
23
|
-
);
|
|
20
|
+
const app = WeftApp.make(ThemeServiceLive);
|
|
21
|
+
const handle = WeftApp.mount(app, App(), document.getElementById("root")!);
|
|
24
22
|
```
|
|
25
23
|
|
|
26
|
-
Provide too little and it is a compile error at
|
|
24
|
+
Provide too little and it is a compile error at `WeftApp.make` — the type of `App()` names exactly which service is missing. This is the same discipline as any Effect program: `R` is a promise the type checker holds you to, discharged at the program's boundary, not sprinkled through the tree.
|
|
27
25
|
|
|
28
|
-
Services flow **down** from
|
|
26
|
+
Services flow **down** from the app's layer to every reader, including across reactive boundaries: a stream woven into a prop carries its own `R`, and a handler that reads a service resolves it from the same context. There is no prop-drilling and no context-provider component — the requirement channel _is_ the wiring.
|
|
29
27
|
|
|
30
|
-
## Layer lifetime
|
|
28
|
+
## Layer lifetime and the app runtime
|
|
31
29
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
`Effect.provide(scopedLayer)` is `acquireUseRelease` sugar: acquire, run the wrapped effect, then release **when that effect completes**. Wrap it directly around `mount`, and the release runs at mount-resolve — while the mounted tree is still reading from the now-disposed service:
|
|
30
|
+
Under the old `mount`/`hydrate` model this was a real footgun. Each call created its own implicit `ManagedRuntime`, and that runtime's effect resolved right after the tree's **initial render** — not when the app stopped running — while streams, event handlers, and forked work kept running on it long after. `Effect.provide(scopedLayer)` is `acquireUseRelease` sugar: acquire, run the wrapped effect, then release **when that effect completes**. Wrapped directly around `mount`, the release ran at mount-resolve — while the mounted tree was still reading from the now-disposed service:
|
|
35
31
|
|
|
36
32
|
```typescript
|
|
37
|
-
// ❌ the layer's finalizers
|
|
38
|
-
// mounted tree
|
|
33
|
+
// ❌ (old API) the layer's finalizers ran the instant runPromise settled, while the
|
|
34
|
+
// mounted tree kept running — every subscription then read a disposed service
|
|
39
35
|
Effect.runPromise(mount(App(), root).pipe(Effect.provide(SomeScopedLayer)));
|
|
40
36
|
```
|
|
41
37
|
|
|
42
38
|
This is exactly what happened with the atom registry layer (`AtomRegistry.layer`, from `effect/unstable/reactivity`) in the [`effect-atom` example](https://github.com/stefvw93/weft/tree/main/examples/effect-atom) (issue #122): every atom-driven region rendered empty, with no error, because the registry the streams read from had already been disposed.
|
|
43
39
|
|
|
44
|
-
|
|
40
|
+
`WeftApp` closes this gap structurally instead of by convention. An app owns exactly **one** lazy `ManagedRuntime`: `WeftApp.make(layer)` builds the layer on the first `mount`/`hydrate`, and it releases only at `WeftApp.dispose(app)` — never when any individual mount's render effect resolves. A scoped layer (`AtomRegistry.layer`, `RouterLive`) therefore just works passed straight to `WeftApp.make`, with no `mountScoped`, no `Effect.never`, and no manual `ManagedRuntime` composition to reach for. See [Provide Services](https://weftui.dev/docs/how-to/provide-services) for the recipes — including `memoMap` sharing across apps and the `Effect.acquireRelease(make, dispose)` pattern for binding an app's own lifetime to an external scope (there is deliberately no `makeScoped`).
|
|
45
41
|
|
|
46
42
|
## The router's render-time context seam
|
|
47
43
|
|
|
48
|
-
A plain `mount`/`hydrate` discharges `R` at
|
|
44
|
+
A plain `WeftApp.mount`/`WeftApp.hydrate` discharges `R` once, at `WeftApp.make`. But under `@weftui/router`, the tree does not render in the context of the effect that called `render` — each request dispatches through platform's HTTP layer in its own managed context, and the reactive outlet drains in the top render context, not in any intermediate node's. Providing a service _ambiently_ around the render would be lost before it reached a route component.
|
|
49
45
|
|
|
50
46
|
So the router exposes an explicit **`context` seam** — a `Layer` threaded to the document shell and every route, layout, and leaf:
|
|
51
47
|
|
|
@@ -77,7 +73,7 @@ class Db extends ServerTag("Db")<Db, { query: (sql: string) => Effect.Effect<Row
|
|
|
77
73
|
## The whole picture
|
|
78
74
|
|
|
79
75
|
- A component reads a service with `yield* Service`; the requirement enters `R`.
|
|
80
|
-
- `R` accumulates through the tree and is discharged **once** — at `
|
|
76
|
+
- `R` accumulates through the tree and is discharged **once** — at `WeftApp.make`, or through the router's `context` seam.
|
|
81
77
|
- The same services flow to the same components on the server and the client, because it is the same tree.
|
|
82
78
|
- `ServerTag` brands the services that must stay server-side, enforced at the `hydrate` boundary.
|
|
83
79
|
|
|
@@ -85,7 +81,7 @@ class Db extends ServerTag("Db")<Db, { query: (sql: string) => Effect.Effect<Row
|
|
|
85
81
|
|
|
86
82
|
- [The Rendering Model](https://weftui.dev/docs/explanation/rendering-model) — why services flow through the tree at all
|
|
87
83
|
- [The Combinator API](https://weftui.dev/docs/explanation/combinator-api) — how `R` accumulates from children and reactive props
|
|
88
|
-
- [Provide Services](https://weftui.dev/docs/how-to/provide-services) — recipes for
|
|
84
|
+
- [Provide Services](https://weftui.dev/docs/how-to/provide-services) — recipes for app layers, scoped layers, and binding an app's lifetime to an external scope
|
|
89
85
|
- [Add Routing](https://weftui.dev/docs/how-to/add-routing) — providing app services through the router `context` seam
|
|
90
86
|
- [Load Data with RPC](https://weftui.dev/docs/how-to/load-data-with-rpc) — where `ServerTag` and the rpc handler Layer meet
|
|
91
87
|
- [`ServerTag` API reference](https://weftui.dev/docs/reference/core#servertag)
|
|
@@ -177,21 +177,21 @@ Router.route("users/:id", {
|
|
|
177
177
|
|
|
178
178
|
## Client setup
|
|
179
179
|
|
|
180
|
-
On the client, provide the `Router` via `RouterLive(def)` and render `RouterApp(def)`. `RouterLive` is a **scoped layer** — it owns the `popstate` listener and the same-origin link-click interceptor — so it must outlive the mount.
|
|
180
|
+
On the client, provide the `Router` via `RouterLive(def)` and render `RouterApp(def)`. `RouterLive` is a **scoped layer** — it owns the `popstate` listener and the same-origin link-click interceptor — so it must outlive the mount. Give it to `WeftApp.make`: the app runtime owns it for the app's lifetime, built lazily on first hydrate and released only at `WeftApp.dispose`. Do not wrap `Effect.provide` around the mount/hydrate call — services come exclusively from the app layer.
|
|
181
181
|
|
|
182
182
|
```typescript
|
|
183
183
|
// entry-client.ts
|
|
184
|
-
import {
|
|
184
|
+
import { WeftApp } from "@weftui/dom/client";
|
|
185
185
|
import { RouterApp, RouterLive } from "@weftui/router/client";
|
|
186
|
-
import {
|
|
186
|
+
import { Effect } from "effect";
|
|
187
187
|
import { App } from "./app";
|
|
188
188
|
|
|
189
189
|
const root = document.getElementById("root")!;
|
|
190
|
-
const
|
|
191
|
-
void
|
|
190
|
+
const app = WeftApp.make(RouterLive(App));
|
|
191
|
+
void Effect.runPromise(WeftApp.hydrate(app, RouterApp(App), root));
|
|
192
192
|
```
|
|
193
193
|
|
|
194
|
-
For a client-only app (no SSR), swap `hydrate` for `mount` — everything else is identical.
|
|
194
|
+
For a client-only app (no SSR), swap `WeftApp.hydrate` for `WeftApp.mount` — everything else is identical.
|
|
195
195
|
|
|
196
196
|
### Link interception
|
|
197
197
|
|
|
@@ -280,7 +280,7 @@ export const handler = RouterServer.toWebHandler(App, { document: documentShell
|
|
|
280
280
|
|
|
281
281
|
### `effect/unstable/httpapi` is the spine
|
|
282
282
|
|
|
283
|
-
The tree is the authoring surface, but `effect/unstable/httpapi`'s `HttpApi` is the **single source of truth** for paths and schemas. Sealing the tree with `Router.router(...)` builds it once (`buildHttpApi`) and stamps it onto `def.httpApi`: a single `"pages"` group with one GET endpoint per leaf at its full path pattern, carrying `
|
|
283
|
+
The tree is the authoring surface, but `effect/unstable/httpapi`'s `HttpApi` is the **single source of truth** for paths and schemas. Sealing the tree with `Router.router(...)` builds it once (`buildHttpApi`) and stamps it onto `def.httpApi`: a single `"pages"` group with one GET endpoint per leaf at its full path pattern, carrying `params: pathSchema`, `query: querySchema`, and a `RouterNotFound → 404` error. Both sides read that one definition, so they always agree:
|
|
284
284
|
|
|
285
285
|
- **Server** — `RouterServer` dispatches through `HttpApiBuilder` (platform owns request→leaf matching, path/query decode, and the 404 status).
|
|
286
286
|
- **Client** — `RouterLive` derives a real `HttpApiClient` from the same `def.httpApi` (exposed as `Router.httpApiClient`) for network work. SPA URL→leaf resolution stays **local** (there is no public client-side "match this URL against my `HttpApi`" utility in platform), fed from the same endpoint definitions so it never drifts from the server.
|
|
@@ -56,7 +56,7 @@ The return type here is `Effect.Effect<Node, never, never>` — itself a valid `
|
|
|
56
56
|
Every component instance is rendered under its own **instance scope** — a child of the
|
|
57
57
|
mount scope created fresh for that instance. Anything bound to the instance scope lives
|
|
58
58
|
exactly as long as the component is mounted and is torn down automatically when the
|
|
59
|
-
component unmounts (or when
|
|
59
|
+
component unmounts (or when its root unmounts via `RootHandle.unmount()`). The renderer
|
|
60
60
|
provides this scope as the ambient `Scope.Scope` while it evaluates the component body,
|
|
61
61
|
so it is already in context when you need it.
|
|
62
62
|
|
|
@@ -235,12 +235,14 @@ const UserAvatar = Component.gen(function* (props: { userId: string }) {
|
|
|
235
235
|
const avatar = UserAvatar({ userId: "123" });
|
|
236
236
|
```
|
|
237
237
|
|
|
238
|
-
|
|
238
|
+
Give the service to the app layer:
|
|
239
239
|
|
|
240
240
|
```typescript
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
241
|
+
import { WeftApp } from "@weftui/dom/client";
|
|
242
|
+
import { Effect } from "effect";
|
|
243
|
+
|
|
244
|
+
const app = WeftApp.make(UserServiceLive);
|
|
245
|
+
void Effect.runPromise(WeftApp.mount(app, App(), document.getElementById("root")!));
|
|
244
246
|
```
|
|
245
247
|
|
|
246
248
|
## Returning fragments
|
|
@@ -88,11 +88,13 @@ export const render = (url: string) =>
|
|
|
88
88
|
|
|
89
89
|
```typescript
|
|
90
90
|
// entry-client.ts — network client posting to /_eui/rpc
|
|
91
|
+
import { WeftApp } from "@weftui/dom/client";
|
|
91
92
|
import { RouterApp, RouterLive } from "@weftui/router/client";
|
|
93
|
+
import { Effect } from "effect";
|
|
92
94
|
import { StockRpcs } from "./data/inventory";
|
|
93
95
|
|
|
94
|
-
const
|
|
95
|
-
void
|
|
96
|
+
const app = WeftApp.make(RouterLive(App, { rpc: { group: StockRpcs } }));
|
|
97
|
+
void Effect.runPromise(WeftApp.hydrate(app, RouterApp(App), root));
|
|
96
98
|
```
|
|
97
99
|
|
|
98
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.
|
|
@@ -2,123 +2,101 @@
|
|
|
2
2
|
title: Provide Services
|
|
3
3
|
order: 12
|
|
4
4
|
section: how-to
|
|
5
|
-
description: Provide plain and scoped Layers to a
|
|
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
|
-
**Goal:** provide a `Layer` to
|
|
10
|
+
**Goal:** provide a `Layer` to a `WeftApp` so its components can read services with `yield* Service`.
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
## Recipe 1 — app layers
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
Provide the layer directly around `mount` and run with `runPromise`. This is the common case and needs nothing else.
|
|
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.
|
|
17
15
|
|
|
18
16
|
```typescript
|
|
19
|
-
import {
|
|
20
|
-
import { Effect
|
|
17
|
+
import { WeftApp } from "@weftui/dom/client";
|
|
18
|
+
import { Effect } from "effect";
|
|
21
19
|
import { App } from "./app";
|
|
22
20
|
import { ThemeServiceLive } from "./theme-service";
|
|
23
21
|
|
|
24
22
|
const root = document.getElementById("root")!;
|
|
25
23
|
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
Effect.runPromise(program);
|
|
24
|
+
const app = WeftApp.make(ThemeServiceLive);
|
|
25
|
+
void Effect.runPromise(WeftApp.mount(app, App(), root));
|
|
29
26
|
```
|
|
30
27
|
|
|
31
|
-
## Recipe 2 — scoped layers
|
|
28
|
+
## Recipe 2 — scoped layers just work
|
|
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.
|
|
32
31
|
|
|
33
|
-
|
|
32
|
+
`AtomRegistry.layer` (from `effect/unstable/reactivity`) is a real scoped layer — its atom subscriptions are fibers forked for the app's whole lifetime:
|
|
34
33
|
|
|
35
34
|
```typescript
|
|
36
|
-
import {
|
|
37
|
-
import {
|
|
35
|
+
import { WeftApp } from "@weftui/dom/client";
|
|
36
|
+
import { Effect } from "effect";
|
|
37
|
+
import { AtomRegistry } from "effect/unstable/reactivity";
|
|
38
38
|
import { App } from "./app";
|
|
39
|
-
import { AppLive } from "./app-live";
|
|
40
|
-
|
|
41
|
-
const root = document.getElementById("root")!;
|
|
42
|
-
|
|
43
|
-
const program = pipe(
|
|
44
|
-
Effect.scoped(
|
|
45
|
-
Effect.gen(function* () {
|
|
46
|
-
yield* mountScoped(App(), root);
|
|
47
|
-
yield* Effect.never; // keeps the region — and AppLive — alive
|
|
48
|
-
}),
|
|
49
|
-
),
|
|
50
|
-
Effect.provide(AppLive), // OUTSIDE the scoped region: outlives initial render
|
|
51
|
-
);
|
|
52
39
|
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
// later, e.g. on a "sign out" action or test teardown:
|
|
56
|
-
// await Effect.runPromise(Fiber.interrupt(fiber));
|
|
40
|
+
const app = WeftApp.make(AtomRegistry.layer);
|
|
41
|
+
void Effect.runPromise(WeftApp.mount(app, App(), document.getElementById("root")!));
|
|
57
42
|
```
|
|
58
43
|
|
|
59
|
-
|
|
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:
|
|
60
45
|
|
|
61
46
|
```typescript
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
const program = pipe(
|
|
65
|
-
Effect.scoped(
|
|
66
|
-
Effect.gen(function* () {
|
|
67
|
-
yield* mountScoped(App(), root);
|
|
68
|
-
yield* Deferred.await(shutdown); // resolves when shutdown is signalled
|
|
69
|
-
}),
|
|
70
|
-
),
|
|
71
|
-
Effect.provide(AppLive),
|
|
72
|
-
);
|
|
73
|
-
Effect.runFork(program);
|
|
74
|
-
|
|
75
|
-
// elsewhere, to request shutdown:
|
|
76
|
-
// await Effect.runPromise(Deferred.succeed(shutdown, undefined));
|
|
47
|
+
const app = WeftApp.make(RouterLive(App, { rpc: { group: StockRpcs } }));
|
|
48
|
+
void Effect.runPromise(WeftApp.hydrate(app, RouterApp(App), root));
|
|
77
49
|
```
|
|
78
50
|
|
|
79
|
-
|
|
51
|
+
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).
|
|
80
52
|
|
|
81
|
-
## Recipe 3 —
|
|
53
|
+
## Recipe 3 — sharing layer memoization with `memoMap`
|
|
82
54
|
|
|
83
|
-
|
|
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:
|
|
84
56
|
|
|
85
57
|
```typescript
|
|
86
|
-
import {
|
|
87
|
-
import {
|
|
88
|
-
import { App } from "./app";
|
|
89
|
-
import { AppLive } from "./app-live";
|
|
90
|
-
|
|
91
|
-
const root = document.getElementById("root")!;
|
|
92
|
-
const runtime = ManagedRuntime.make(AppLive);
|
|
58
|
+
import { WeftApp } from "@weftui/dom/client";
|
|
59
|
+
import { Layer } from "effect";
|
|
93
60
|
|
|
94
|
-
|
|
61
|
+
const memoMap = Layer.makeMemoMap();
|
|
95
62
|
|
|
96
|
-
|
|
97
|
-
|
|
63
|
+
const appA = WeftApp.make(SharedLive, { memoMap });
|
|
64
|
+
const appB = WeftApp.make(SharedLive, { memoMap });
|
|
98
65
|
```
|
|
99
66
|
|
|
100
|
-
|
|
67
|
+
Most apps have exactly one `WeftApp` and never need this option.
|
|
101
68
|
|
|
102
|
-
##
|
|
69
|
+
## Recipe 4 — binding an app's lifetime to a scope
|
|
103
70
|
|
|
104
|
-
|
|
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`:
|
|
105
72
|
|
|
106
73
|
```typescript
|
|
107
|
-
|
|
108
|
-
|
|
74
|
+
import { Effect } from "effect";
|
|
75
|
+
import { WeftApp } from "@weftui/dom/client";
|
|
76
|
+
import { AppLive } from "./app-live";
|
|
77
|
+
|
|
78
|
+
const acquireApp = Effect.acquireRelease(
|
|
79
|
+
Effect.sync(() => WeftApp.make(AppLive)),
|
|
80
|
+
(app) => WeftApp.dispose(app),
|
|
81
|
+
);
|
|
109
82
|
```
|
|
110
83
|
|
|
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).
|
|
85
|
+
|
|
86
|
+
## Anti-pattern: `Effect.provide` around the mount call
|
|
87
|
+
|
|
111
88
|
```typescript
|
|
112
|
-
// ❌
|
|
113
|
-
//
|
|
114
|
-
|
|
89
|
+
// ❌ does nothing useful: WeftApp.mount's R is always `never`, and services
|
|
90
|
+
// come exclusively from the app layer — a wrapped Effect.provide never
|
|
91
|
+
// reaches components, handlers, or stream subscriptions
|
|
92
|
+
Effect.runPromise(pipe(WeftApp.mount(app, App(), root), Effect.provide(SomeLayer)));
|
|
115
93
|
```
|
|
116
94
|
|
|
117
|
-
|
|
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`.
|
|
118
96
|
|
|
119
97
|
## See also
|
|
120
98
|
|
|
121
|
-
- [
|
|
122
|
-
- [
|
|
123
|
-
- [
|
|
124
|
-
- [examples/
|
|
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
|
|
@@ -12,7 +12,7 @@ Weft renders on the server and **hydrates** on the client: the server produces H
|
|
|
12
12
|
## The two halves
|
|
13
13
|
|
|
14
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 `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.
|
|
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
16
|
|
|
17
17
|
```typescript
|
|
18
18
|
// server entry
|
|
@@ -25,12 +25,13 @@ export const render = (): Promise<string> => Effect.runPromise(renderToStringHyd
|
|
|
25
25
|
|
|
26
26
|
```typescript
|
|
27
27
|
// client entry
|
|
28
|
-
import {
|
|
28
|
+
import { WeftApp } from "@weftui/dom/client";
|
|
29
29
|
import { Effect } from "effect";
|
|
30
30
|
import { App } from "./app";
|
|
31
31
|
|
|
32
32
|
const root = document.getElementById("root")!;
|
|
33
|
-
|
|
33
|
+
const app = WeftApp.make();
|
|
34
|
+
void Effect.runPromise(WeftApp.hydrate(app, App(), root));
|
|
34
35
|
```
|
|
35
36
|
|
|
36
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.
|
package/docs/index.md
CHANGED
|
@@ -10,7 +10,7 @@ The docs follow the [Diátaxis](https://diataxis.fr) model. Pick your entry poin
|
|
|
10
10
|
|
|
11
11
|
**[→ Tutorial](https://weftui.dev/docs/tutorial/01-your-first-app)** — a four-step guided path from a static component to a server-rendered, error-handled app. Start here if you are new to Weft:
|
|
12
12
|
|
|
13
|
-
1. [Your First App](https://weftui.dev/docs/tutorial/01-your-first-app) — `h` and `
|
|
13
|
+
1. [Your First App](https://weftui.dev/docs/tutorial/01-your-first-app) — `h` and `WeftApp`
|
|
14
14
|
2. [Reactivity](https://weftui.dev/docs/tutorial/02-reactivity) — `SubscriptionRef` and streams
|
|
15
15
|
3. [Services and Async](https://weftui.dev/docs/tutorial/03-services-and-async) — handlers, services, async loading
|
|
16
16
|
4. [Errors and Server Rendering](https://weftui.dev/docs/tutorial/04-errors-and-server) — boundaries and SSR
|
|
@@ -31,7 +31,7 @@ New to the model itself? Read [The Rendering Model](https://weftui.dev/docs/expl
|
|
|
31
31
|
Three published packages make up Weft's public API, plus one build-time plugin:
|
|
32
32
|
|
|
33
33
|
- **`@weftui/core`** — element builders (`h`), components, sources/streams, and boundaries. Start here.
|
|
34
|
-
- **`@weftui/dom`** — the renderer: `./client` (`mount`/`hydrate`) and `./server` (`renderToString*`).
|
|
34
|
+
- **`@weftui/dom`** — the renderer: `./client` (`WeftApp.mount`/`WeftApp.hydrate`) and `./server` (`renderToString*`).
|
|
35
35
|
- **`@weftui/router`** — universal nested routing, `Router.lazy`, and the rpc seam.
|
|
36
36
|
- **`@weftui/vite`** — a build-time Vite plugin (tooling, not a runtime API).
|
|
37
37
|
|