raf-effect-atom 0.2.0 → 0.2.1
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/CHANGELOG.md +8 -2
- package/README.md +1 -1
- package/dist/ReactRouter.d.mts +46 -3
- package/dist/ReactRouter.mjs +73 -6
- package/dist/RegistryProvider.d.mts +3 -0
- package/dist/RegistryProvider.mjs +5 -1
- package/dist/internal/registry.mjs +15 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.2.1](https://github.com/KATT/raf-effect-atom/compare/raf-effect-atom%400.2.0...raf-effect-atom%400.2.1) (2026-10-23)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
* **raf-effect-atom:** run React Router loaders in the browser after the first page ([bee00a9](https://github.com/KATT/raf-effect-atom/commit/bee00a981fd24fcdc9d746053dcd73dbc7ebc853)), references [#41](https://github.com/KATT/raf-effect-atom/issues/41) [#41](https://github.com/KATT/raf-effect-atom/issues/41)
|
|
8
|
+
|
|
3
9
|
## [0.2.0](https://github.com/KATT/raf-effect-atom/compare/raf-effect-atom%400.1.0...raf-effect-atom%400.2.0) (2026-10-23)
|
|
4
10
|
|
|
5
11
|
### ⚠ BREAKING CHANGES
|
|
6
12
|
|
|
7
|
-
|
|
13
|
+
- **raf-effect-atom:** `dehydrate()` from `raf-effect-atom/next` returns a
|
|
8
14
|
|
|
9
15
|
### Features
|
|
10
16
|
|
|
11
|
-
|
|
17
|
+
- **raf-effect-atom:** seed each request registry, async seeds included ([62b6180](https://github.com/KATT/raf-effect-atom/commit/62b618098df87e711ddb4c1d633e191ba3353438))
|
|
12
18
|
|
|
13
19
|
## 0.1.0 (2026-10-23)
|
|
14
20
|
|
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ Each framework has a subpath that gives every server request its own registry an
|
|
|
53
53
|
|
|
54
54
|
- `raf-effect-atom/tanstack-router`, for TanStack Router and TanStack Start: `setupRouterAtoms({ router, registry })` in `getRouter`, with `Loader.make(registry)` on the router context.
|
|
55
55
|
- `raf-effect-atom/next`, for the App Router: `prefetch`, `ensure` and `fetch` in server components, which pass the handles to client components as props, and `<HydrateAtoms state={await dehydrate()}>` around them. `makeRequestAtoms({ seed })` makes the same functions over a registry seeded from the request, such as with its cookie.
|
|
56
|
-
- `raf-effect-atom/react-router`, for Framework Mode: `atomsMiddleware(atomsContext)` as the root middleware, loaders that return `atoms.dehydrate()` with their handles, and `HydrationBoundary` in the route component.
|
|
56
|
+
- `raf-effect-atom/react-router`, for Framework Mode: `atomsMiddleware(atomsContext)` as the root middleware and `clientAtomsMiddleware(atomsContext)` as its client middleware, loaders that return `atoms.dehydrate()` with their handles and double as client loaders, and `HydrationBoundary` in the route component.
|
|
57
57
|
|
|
58
58
|
In development, `useAtomResult` reports a read that starts an atom nothing started, a request waterfall. `<WaterfallDetection mode="throw">` at the root makes it throw to the nearest error boundary. `useWaterfall(source, ...args)` starts an atom whose input exists only in render, marks it as a known waterfall, and returns its handle.
|
|
59
59
|
|
package/dist/ReactRouter.d.mts
CHANGED
|
@@ -3,7 +3,9 @@ import { RegistryProvider } from "./RegistryProvider.mjs";
|
|
|
3
3
|
import * as EffectHydration from "effect/reactivity/Hydration";
|
|
4
4
|
import * as AtomRegistry from "effect/reactivity/AtomRegistry";
|
|
5
5
|
/**
|
|
6
|
-
* One request's registry, with the loader verbs over it.
|
|
6
|
+
* One request's registry, with the loader verbs over it. In a client loader,
|
|
7
|
+
* the browser's registry, whose `dehydrate` returns nothing: its atoms are
|
|
8
|
+
* already where the components read them.
|
|
7
9
|
*
|
|
8
10
|
* @stability unstable
|
|
9
11
|
* @category models
|
|
@@ -71,8 +73,7 @@ export interface AtomsMiddlewareOptions {
|
|
|
71
73
|
* **Gotchas**
|
|
72
74
|
*
|
|
73
75
|
* It's server middleware: export it as the root route's `middleware`, not
|
|
74
|
-
* `clientMiddleware
|
|
75
|
-
* the registry.
|
|
76
|
+
* `clientMiddleware`, which takes `clientAtomsMiddleware`.
|
|
76
77
|
*
|
|
77
78
|
* **Example**
|
|
78
79
|
*
|
|
@@ -91,4 +92,46 @@ export interface AtomsMiddlewareOptions {
|
|
|
91
92
|
* @since 0.1.0
|
|
92
93
|
*/
|
|
93
94
|
export declare const atomsMiddleware: (context: AtomsContext, options?: AtomsMiddlewareOptions) => <Result>(args: MiddlewareArgs, next: () => Promise<Result>) => Promise<Result>;
|
|
95
|
+
/**
|
|
96
|
+
* Creates a root client middleware that gives client loaders the browser's
|
|
97
|
+
* registry, under the context `atomsMiddleware` uses on the server.
|
|
98
|
+
*
|
|
99
|
+
* **When to use**
|
|
100
|
+
*
|
|
101
|
+
* Export it as the root route's `clientMiddleware`, and each route's loader
|
|
102
|
+
* function as both its `loader` and its `clientLoader`. The first page's loaders run on
|
|
103
|
+
* the server and stream their atoms in; after that, navigations run the
|
|
104
|
+
* loaders in the browser. `ensure` then renders what the browser's registry
|
|
105
|
+
* holds without a request, and a link's prefetch doesn't fetch a route's
|
|
106
|
+
* `.data` that the click would fetch again.
|
|
107
|
+
*
|
|
108
|
+
* **Details**
|
|
109
|
+
*
|
|
110
|
+
* The registry is the one the mounted `RegistryProvider` provides. Its
|
|
111
|
+
* `dehydrate` returns nothing, so a loader's data has the same shape in
|
|
112
|
+
* the browser and on the server, and `HydrationBoundary` has nothing to do.
|
|
113
|
+
*
|
|
114
|
+
* **Gotchas**
|
|
115
|
+
*
|
|
116
|
+
* The atoms run in the browser, so their RPC protocol must reach the server
|
|
117
|
+
* from there, as it does for anything a component starts.
|
|
118
|
+
*
|
|
119
|
+
* **Example**
|
|
120
|
+
*
|
|
121
|
+
* ```ts
|
|
122
|
+
* // app/root.tsx
|
|
123
|
+
* export const middleware = [atomsMiddleware(atomsContext)]
|
|
124
|
+
* export const clientMiddleware = [clientAtomsMiddleware(atomsContext)]
|
|
125
|
+
*
|
|
126
|
+
* // app/routes/issue.tsx: one function for both, since React Router
|
|
127
|
+
* // removes `loader` from the browser's bundle.
|
|
128
|
+
* export const loader = load
|
|
129
|
+
* export const clientLoader = load
|
|
130
|
+
* ```
|
|
131
|
+
*
|
|
132
|
+
* @stability unstable
|
|
133
|
+
* @category integrations
|
|
134
|
+
* @since 0.2.0
|
|
135
|
+
*/
|
|
136
|
+
export declare const clientAtomsMiddleware: (context: AtomsContext) => <Result>(args: MiddlewareArgs, next: () => Promise<Result>) => Promise<Result>;
|
|
94
137
|
export { RegistryProvider };
|
package/dist/ReactRouter.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { dehydrate } from "./Hydration.mjs";
|
|
2
2
|
import { make, settled } from "./RequestRegistry.mjs";
|
|
3
3
|
import { make as make$1 } from "./Loader.mjs";
|
|
4
|
+
import { current } from "./internal/registry.mjs";
|
|
4
5
|
import { RegistryProvider } from "./RegistryProvider.mjs";
|
|
5
6
|
import * as Predicate from "effect/Predicate";
|
|
6
7
|
// src/ReactRouter.ts
|
|
@@ -10,8 +11,11 @@ import * as Predicate from "effect/Predicate";
|
|
|
10
11
|
* A root `middleware` gives each server request its own registry, under a
|
|
11
12
|
* router context the app creates. Loaders start atoms with its verbs and
|
|
12
13
|
* return its `dehydrate()` with their data; the route component hydrates it
|
|
13
|
-
* with `HydrationBoundary`, inside the root's `RegistryProvider`.
|
|
14
|
-
*
|
|
14
|
+
* with `HydrationBoundary`, inside the root's `RegistryProvider`. A root
|
|
15
|
+
* `clientMiddleware` puts the browser's registry under the same context, so
|
|
16
|
+
* one function can be both the route's `loader` and its `clientLoader`: after the first
|
|
17
|
+
* page, loaders run in the browser, where `ensure` finds what the registry
|
|
18
|
+
* already holds. Nothing is imported from `react-router`.
|
|
15
19
|
*
|
|
16
20
|
* **Example**
|
|
17
21
|
*
|
|
@@ -26,14 +30,18 @@ import * as Predicate from "effect/Predicate";
|
|
|
26
30
|
* import { atomsMiddleware } from "raf-effect-atom/react-router"
|
|
27
31
|
*
|
|
28
32
|
* export const middleware = [atomsMiddleware(atomsContext)]
|
|
33
|
+
* export const clientMiddleware = [clientAtomsMiddleware(atomsContext)]
|
|
29
34
|
*
|
|
30
35
|
* // app/routes/issue.tsx
|
|
31
|
-
*
|
|
36
|
+
* async function load(args: Route.LoaderArgs) {
|
|
32
37
|
* const atoms = args.context.get(atomsContext)
|
|
33
38
|
* const handle = await atoms.ensure(issue, Number(args.params.id))
|
|
34
39
|
* return { atoms: atoms.dehydrate(), issue: handle }
|
|
35
40
|
* }
|
|
36
41
|
*
|
|
42
|
+
* export const loader = load
|
|
43
|
+
* export const clientLoader = load
|
|
44
|
+
*
|
|
37
45
|
* export default function IssueRoute(props: Route.ComponentProps) {
|
|
38
46
|
* return (
|
|
39
47
|
* <HydrationBoundary state={props.loaderData.atoms}>
|
|
@@ -64,8 +72,7 @@ import * as Predicate from "effect/Predicate";
|
|
|
64
72
|
* **Gotchas**
|
|
65
73
|
*
|
|
66
74
|
* It's server middleware: export it as the root route's `middleware`, not
|
|
67
|
-
* `clientMiddleware
|
|
68
|
-
* the registry.
|
|
75
|
+
* `clientMiddleware`, which takes `clientAtomsMiddleware`.
|
|
69
76
|
*
|
|
70
77
|
* **Example**
|
|
71
78
|
*
|
|
@@ -135,4 +142,64 @@ const disposeAfter = (body, registry) => {
|
|
|
135
142
|
}
|
|
136
143
|
});
|
|
137
144
|
};
|
|
138
|
-
|
|
145
|
+
const browserAtoms = /* @__PURE__ */ new WeakMap();
|
|
146
|
+
const browserAtomsOf = (registry) => {
|
|
147
|
+
let atoms = browserAtoms.get(registry);
|
|
148
|
+
if (atoms === void 0) {
|
|
149
|
+
atoms = {
|
|
150
|
+
...make$1(registry),
|
|
151
|
+
dehydrate: () => [],
|
|
152
|
+
registry
|
|
153
|
+
};
|
|
154
|
+
browserAtoms.set(registry, atoms);
|
|
155
|
+
}
|
|
156
|
+
return atoms;
|
|
157
|
+
};
|
|
158
|
+
/**
|
|
159
|
+
* Creates a root client middleware that gives client loaders the browser's
|
|
160
|
+
* registry, under the context `atomsMiddleware` uses on the server.
|
|
161
|
+
*
|
|
162
|
+
* **When to use**
|
|
163
|
+
*
|
|
164
|
+
* Export it as the root route's `clientMiddleware`, and each route's loader
|
|
165
|
+
* function as both its `loader` and its `clientLoader`. The first page's loaders run on
|
|
166
|
+
* the server and stream their atoms in; after that, navigations run the
|
|
167
|
+
* loaders in the browser. `ensure` then renders what the browser's registry
|
|
168
|
+
* holds without a request, and a link's prefetch doesn't fetch a route's
|
|
169
|
+
* `.data` that the click would fetch again.
|
|
170
|
+
*
|
|
171
|
+
* **Details**
|
|
172
|
+
*
|
|
173
|
+
* The registry is the one the mounted `RegistryProvider` provides. Its
|
|
174
|
+
* `dehydrate` returns nothing, so a loader's data has the same shape in
|
|
175
|
+
* the browser and on the server, and `HydrationBoundary` has nothing to do.
|
|
176
|
+
*
|
|
177
|
+
* **Gotchas**
|
|
178
|
+
*
|
|
179
|
+
* The atoms run in the browser, so their RPC protocol must reach the server
|
|
180
|
+
* from there, as it does for anything a component starts.
|
|
181
|
+
*
|
|
182
|
+
* **Example**
|
|
183
|
+
*
|
|
184
|
+
* ```ts
|
|
185
|
+
* // app/root.tsx
|
|
186
|
+
* export const middleware = [atomsMiddleware(atomsContext)]
|
|
187
|
+
* export const clientMiddleware = [clientAtomsMiddleware(atomsContext)]
|
|
188
|
+
*
|
|
189
|
+
* // app/routes/issue.tsx: one function for both, since React Router
|
|
190
|
+
* // removes `loader` from the browser's bundle.
|
|
191
|
+
* export const loader = load
|
|
192
|
+
* export const clientLoader = load
|
|
193
|
+
* ```
|
|
194
|
+
*
|
|
195
|
+
* @stability unstable
|
|
196
|
+
* @category integrations
|
|
197
|
+
* @since 0.2.0
|
|
198
|
+
*/
|
|
199
|
+
const clientAtomsMiddleware = (context) => (args, next) => {
|
|
200
|
+
const registry = current();
|
|
201
|
+
if (registry === void 0) throw new Error("clientAtomsMiddleware found no mounted RegistryProvider: render raf-effect-atom/react-router's RegistryProvider around the app in the root layout.");
|
|
202
|
+
args.context.set(context, browserAtomsOf(registry));
|
|
203
|
+
return next();
|
|
204
|
+
};
|
|
205
|
+
export { RegistryProvider, atomsMiddleware, clientAtomsMiddleware };
|
|
@@ -18,6 +18,9 @@ import * as React from "react";
|
|
|
18
18
|
* It's read once, when the registry is made. The server's request registry
|
|
19
19
|
* lives for one request, so it has none.
|
|
20
20
|
*
|
|
21
|
+
* In the browser, React Router's `clientAtomsMiddleware` gives client
|
|
22
|
+
* loaders the registry of the mounted provider.
|
|
23
|
+
*
|
|
21
24
|
* **Example**
|
|
22
25
|
*
|
|
23
26
|
* ```tsx
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { make } from "./internal/registry.mjs";
|
|
2
|
+
import { make, mount } from "./internal/registry.mjs";
|
|
3
3
|
import { RegistryContext } from "@effect/atom-react";
|
|
4
4
|
import * as React from "react";
|
|
5
5
|
import { jsx } from "react/jsx-runtime";
|
|
@@ -29,6 +29,9 @@ import { jsx } from "react/jsx-runtime";
|
|
|
29
29
|
* It's read once, when the registry is made. The server's request registry
|
|
30
30
|
* lives for one request, so it has none.
|
|
31
31
|
*
|
|
32
|
+
* In the browser, React Router's `clientAtomsMiddleware` gives client
|
|
33
|
+
* loaders the registry of the mounted provider.
|
|
34
|
+
*
|
|
32
35
|
* **Example**
|
|
33
36
|
*
|
|
34
37
|
* ```tsx
|
|
@@ -47,6 +50,7 @@ import { jsx } from "react/jsx-runtime";
|
|
|
47
50
|
*/
|
|
48
51
|
const RegistryProvider = (props) => {
|
|
49
52
|
const [registry] = React.useState(() => make({ defaultIdleTTL: props.defaultIdleTTL }));
|
|
53
|
+
React.useEffect(() => mount(registry), [registry]);
|
|
50
54
|
return /* @__PURE__ */ jsx(RegistryContext.Provider, {
|
|
51
55
|
value: registry,
|
|
52
56
|
children: props.children
|
|
@@ -13,4 +13,18 @@ const make = (options) => isServer() ? make$1({ seed: options?.seed }) : AtomReg
|
|
|
13
13
|
defaultIdleTTL: options?.defaultIdleTTL ?? defaultIdleTTL,
|
|
14
14
|
scheduleTask
|
|
15
15
|
});
|
|
16
|
-
|
|
16
|
+
let mounted;
|
|
17
|
+
/**
|
|
18
|
+
* Records the browser registry a `RegistryProvider` provides while it's
|
|
19
|
+
* mounted, for code outside React, such as React Router's client
|
|
20
|
+
* middleware. Returns the cleanup that forgets it.
|
|
21
|
+
*/
|
|
22
|
+
const mount = (registry) => {
|
|
23
|
+
mounted = registry;
|
|
24
|
+
return () => {
|
|
25
|
+
if (mounted === registry) mounted = void 0;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
/** The browser registry of the mounted `RegistryProvider`, if one is. */
|
|
29
|
+
const current = () => mounted;
|
|
30
|
+
export { current, isServer, make, mount };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "raf-effect-atom",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Render-as-you-fetch for Effect Atom: start atoms from route loaders and stream them into the browser, typed failures intact",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"effect",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"@types/react-dom": "19.3.0",
|
|
59
59
|
"@vitest/coverage-v8": "5.0.5",
|
|
60
60
|
"effect": "4.0.6",
|
|
61
|
-
"effect-atom-boundary": "0.1.
|
|
61
|
+
"effect-atom-boundary": "0.1.1",
|
|
62
62
|
"happy-dom": "21.0.4",
|
|
63
63
|
"react": "19.3.1",
|
|
64
64
|
"react-dom": "19.3.1",
|