rxfy-react 0.2.0 → 1.0.0-rc.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/README.md +390 -20
- package/dist/chunk-BO3M5NLW.js +42 -0
- package/dist/index.cjs +9705 -32
- package/dist/index.d.ts +79 -29
- package/dist/index.js +9684 -24
- package/dist/next.cjs +83 -0
- package/dist/next.d.ts +8 -0
- package/dist/next.js +49 -0
- package/package.json +40 -21
- package/dist/index.d.cts +0 -32
package/README.md
CHANGED
|
@@ -1,36 +1,406 @@
|
|
|
1
1
|
# rxfy-react
|
|
2
2
|
|
|
3
|
-
`rxfy-react`
|
|
3
|
+
`rxfy-react` is the official React bindings for [`rxfy`](../rxfy/README.md). Subscribe React components to normalized entities; each renders the one shared copy and updates live.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Install
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
```bash
|
|
8
|
+
npm install rxfy rxfy-react
|
|
9
|
+
# or: pnpm add rxfy rxfy-react
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Peer dependencies
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"@types/react": "^18.0.0 || ^19.0.0",
|
|
17
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
18
|
+
"react-dom": "^18.0.0 || ^19.0.0",
|
|
19
|
+
"rxfy": "*",
|
|
20
|
+
"lodash": "^4.0.0",
|
|
21
|
+
"next": ">=14"
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`next` is **optional**; only needed for the `rxfy-react/next` subpath (Next.js App Router streaming).
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Setup
|
|
30
|
+
|
|
31
|
+
Wrap your app (or the relevant subtree) with `StoreProvider`. This creates the model registry that `useStateData` and `useModelStore` write to and read from.
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
import { StoreProvider } from "rxfy-react";
|
|
35
|
+
import { createRoot } from "react-dom/client";
|
|
36
|
+
|
|
37
|
+
createRoot(document.getElementById("root")!).render(
|
|
38
|
+
<StoreProvider>
|
|
39
|
+
<App />
|
|
40
|
+
</StoreProvider>,
|
|
41
|
+
);
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Signature:**
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
function StoreProvider(props: PropsWithChildren<{
|
|
48
|
+
ssr?: boolean; // enables server-side fetch-and-suspend in useStateData
|
|
49
|
+
registry?: IModelRegistry; // per-request registry created by server code (for dehydrate)
|
|
50
|
+
dehydratedState?: DehydratedState; // snapshot for custom transports; usually unnecessary, see below
|
|
51
|
+
}>): JSX.Element
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
All three props exist for [SSR](#server-side-rendering); a plain client-only app uses none of them. On the client the provider automatically ingests `window.__RXFY_SSR__` chunks: both the snapshot injected by `hydrationScript` and the streamed pushes from `<HydrationStream />`, including chunks arriving after hydration starts. `dehydratedState` is only needed when the snapshot travels by some other channel (a framework loader, tests).
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## High-level hooks
|
|
59
|
+
|
|
60
|
+
### `useStateData`
|
|
61
|
+
|
|
62
|
+
Fetches data, normalizes entities into model stores, and returns a `StateHandle`. **`data$` emits the normalized query shape: entity ids, not entities** (`array` fields → `string[]`, `single` fields → `string`). Render lists by id and read entity data through [`useModelStore`](#usemodelstore); that's the only place entity values live, so a stale read is impossible by construction.
|
|
63
|
+
|
|
64
|
+
`fetchFn` returns the full fetch shape (entities) and receives an `AbortSignal` that fires on cleanup. `mutations` and `set` also operate on full entities: rxfy denormalizes the current ids into the freshest store values, runs your reducer, and normalizes the result back, so one call updates both membership and entity data.
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
import { useMemo, useState } from "react";
|
|
68
|
+
import { useStateData, useModelStore, Pending } from "rxfy-react";
|
|
69
|
+
import { todosState, fetchTodos, TodoModel } from "./todos";
|
|
70
|
+
|
|
71
|
+
function TodoApp() {
|
|
72
|
+
const [filter, setFilter] = useState<"all" | "active" | "done">("all");
|
|
73
|
+
const params = useMemo(() => ({ filter }), [filter]);
|
|
74
|
+
|
|
75
|
+
const { data$, mutations, reload } = useStateData(todosState, fetchTodos, params);
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<Pending value$={data$} pending={<p>Loading...</p>}>
|
|
79
|
+
{({ todos }) => (
|
|
80
|
+
<>
|
|
81
|
+
<ul>{todos.map((id) => <TodoItem key={id} id={id} />)}</ul>
|
|
82
|
+
<button onClick={() => mutations.addTodo({ id: crypto.randomUUID(), title: "new", done: false })}>
|
|
83
|
+
Add
|
|
84
|
+
</button>
|
|
85
|
+
<button onClick={reload}>Reload</button>
|
|
86
|
+
</>
|
|
87
|
+
)}
|
|
88
|
+
</Pending>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function TodoItem({ id }: { id: string }) {
|
|
93
|
+
const store = useModelStore(TodoModel);
|
|
94
|
+
const todo$ = useMemo(() => store.get(id), [store, id]);
|
|
95
|
+
return <Pending value$={todo$}>{(todo) => <li>{todo.title}</li>}</Pending>;
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Signature:**
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
function useStateData<TParams, TShape, TMutations>(
|
|
103
|
+
state: StateDescriptor<TParams, TShape, TMutations>,
|
|
104
|
+
fetchFn: (params: TParams, signal: AbortSignal) => Promise<TShape>,
|
|
105
|
+
params: TParams,
|
|
106
|
+
): StateHandle<TShape, TMutations>
|
|
8
107
|
|
|
9
|
-
|
|
108
|
+
// StateHandle<TShape, TMutations>: {
|
|
109
|
+
// data$: Observable<QueryShapeOf<TShape>>; // ids only
|
|
110
|
+
// set: (value: TShape | ((prev: TShape) => TShape)) => void; // full entities
|
|
111
|
+
// reload: () => void;
|
|
112
|
+
// mutations: BoundMutations<TShape, TMutations>; // full entities
|
|
113
|
+
// }
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Caching semantics** (states with a `key`): results, mutations, and `set` write through to the registry's query cache, so a remount with the same params starts from the cached ids without re-fetching, while entity values always come live from model stores (a websocket-style `store.set` between mounts is never clobbered). `reload()` deletes the cache entry and re-fetches. Keyless states skip the cache entirely and fetch per mount.
|
|
117
|
+
|
|
118
|
+
**During SSR** (inside `<StoreProvider ssr>` on the server): a cache miss calls `fetchFn` and suspends until it settles; concurrent components with the same key share one fetch. Rejections are captured and hydrate as rejected state; `<Pending rejected>` renders them with a working retry. See [Server-side rendering](#server-side-rendering).
|
|
119
|
+
|
|
120
|
+
### `useModelStore`
|
|
121
|
+
|
|
122
|
+
Returns the `ModelStore` for a model descriptor. Lets a component subscribe to a single normalized entity that was populated by `useStateData` or a direct `store.set` call, without re-fetching the full list.
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
import { useMemo } from "react";
|
|
126
|
+
import { useModelStore, Pending } from "rxfy-react";
|
|
127
|
+
import { TodoModel } from "./models";
|
|
128
|
+
|
|
129
|
+
function TodoItem({ id }: { id: string }) {
|
|
130
|
+
const store = useModelStore(TodoModel);
|
|
131
|
+
const todo$ = useMemo(() => store.get(id), [store, id]);
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<Pending value$={todo$}>
|
|
135
|
+
{(todo) => <li>{todo.title}</li>}
|
|
136
|
+
</Pending>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**Signature:**
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
function useModelStore<T>(descriptor: ModelDescriptor<T>): ModelStore<T>
|
|
145
|
+
// ModelStore<T>: {
|
|
146
|
+
// get(key: string): Observable<T>; // reactive read; emits on every change
|
|
147
|
+
// set(key, val): void;
|
|
148
|
+
// setMany(items): void;
|
|
149
|
+
// getValue(key: string): T | undefined; // synchronous read
|
|
150
|
+
// entity(key: string): IAtom<T>; // writable handle over one entity's cell
|
|
151
|
+
// valueEntries(): [string, T][];
|
|
152
|
+
// added$: Observable<string>; // a key, the first time its entity appears
|
|
153
|
+
// }
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
> **Note:** `store.get(id)` returns an Observable that never emits until `set` or `setMany` is called for that key. It stays in pending state until data arrives, typically populated by a `useStateData` call that includes this model in its `model` field.
|
|
10
157
|
|
|
11
|
-
|
|
12
|
-
- <Edge /> — useEdge wrapped with render props pattern
|
|
158
|
+
### `useAtom`
|
|
13
159
|
|
|
14
|
-
|
|
160
|
+
Binds any `IAtom<T>` (a plain `Atom`, a field `Lens`, or a `ModelStore.entity(id)` handle) to React as `[value, setValue]`. Paired with `store.entity(id)` and `keyLens`, it gives two-way form binding that stays in sync across every component subscribed to that entity: typing writes through the field `Lens` into the entity cell, so the edit reaches the list row, the detail header, and any other subscriber with no re-fetch.
|
|
15
161
|
|
|
16
162
|
```tsx
|
|
17
|
-
import PQueue from "p-queue";
|
|
18
163
|
import { useMemo } from "react";
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
164
|
+
import { createLens, keyLens } from "rxfy";
|
|
165
|
+
import { useAtom, useModelStore } from "rxfy-react";
|
|
166
|
+
import { TodoModel } from "./models";
|
|
167
|
+
|
|
168
|
+
function EditTitle({ id }: { id: string }) {
|
|
169
|
+
const store = useModelStore(TodoModel);
|
|
170
|
+
const todo$ = useMemo(() => store.entity(id), [store, id]); // IAtom<Todo>
|
|
171
|
+
const title$ = useMemo(() => createLens(todo$, keyLens("title")), [todo$]);
|
|
172
|
+
const [title, setTitle] = useAtom(title$);
|
|
173
|
+
|
|
174
|
+
return <input value={title} onChange={(e) => setTitle(e.target.value)} />;
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**Signature:**
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
function useAtom<T>(atom$: IAtom<T>): [T, (value: T) => void]
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
> `atom$` must be referentially stable across renders; wrap `store.entity(id)` and the `Lens` in `useMemo` as above. `store.entity(id)` assumes the entity is already loaded.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Rendering helpers
|
|
189
|
+
|
|
190
|
+
### `Pending`
|
|
191
|
+
|
|
192
|
+
Subscribes to any `ObservableLike<T>` and renders the appropriate UI for pending, rejected, or fulfilled state. The `rejected` render prop receives the rejected `IWrapped` (`{ type, error }`). If `rejected` is not provided, it defaults to rendering nothing and logging the error to `console.error`.
|
|
193
|
+
|
|
194
|
+
Reload is not carried on the status object: get it from the `useStateData` handle's `reload()` (or `getAttachedReload(source$)` from `rxfy` for a standalone observable).
|
|
195
|
+
|
|
196
|
+
```tsx
|
|
197
|
+
import { Pending, useStateData } from "rxfy-react";
|
|
198
|
+
|
|
199
|
+
const { data$, reload } = useStateData(todosState, fetchTodos, params);
|
|
200
|
+
|
|
201
|
+
<Pending
|
|
202
|
+
value$={data$}
|
|
203
|
+
pending={<p>Loading...</p>}
|
|
204
|
+
rejected={({ error }) => (
|
|
205
|
+
<p>
|
|
206
|
+
Error: {String(error)} <button onClick={reload}>Retry</button>
|
|
207
|
+
</p>
|
|
208
|
+
)}
|
|
209
|
+
>
|
|
210
|
+
{(value) => <div>{JSON.stringify(value)}</div>}
|
|
211
|
+
</Pending>
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**Signature:**
|
|
215
|
+
|
|
216
|
+
```tsx
|
|
217
|
+
function Pending<T>(props: {
|
|
218
|
+
value$: ObservableLike<T>;
|
|
219
|
+
pending?: IRenderable<void>;
|
|
220
|
+
rejected?: IRenderable<IWrapped<T, StatusEnum.REJECTED>>; // { type, error }
|
|
221
|
+
children: IRenderable<T>;
|
|
222
|
+
getDefaultValue?: () => T;
|
|
223
|
+
}): JSX.Element
|
|
224
|
+
|
|
225
|
+
// IRenderable<T> = React.ReactNode | ((data: T) => React.ReactNode)
|
|
226
|
+
// ObservableLike<T> = Observable<T> | T
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Low-level hooks
|
|
232
|
+
|
|
233
|
+
### `usePending`
|
|
234
|
+
|
|
235
|
+
Tracks any `ObservableLike<T>` and returns the current [`IWrapped<T>`](../rxfy/README.md), narrowed to `PENDING` / `FULFILLED` / `REJECTED` (never `IDLE`). This is the hook powering `<Pending>`. Narrow on `type` to read `value` (only on `FULFILLED`) or `error` (only on `REJECTED`). Pass `getDefaultValue` to start `FULFILLED` instead of `PENDING`.
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
import { usePending } from "rxfy-react";
|
|
239
|
+
import { StatusEnum } from "rxfy";
|
|
240
|
+
|
|
241
|
+
const wrapped = usePending(data$);
|
|
242
|
+
if (wrapped.type === StatusEnum.PENDING) return <span>…</span>;
|
|
243
|
+
if (wrapped.type === StatusEnum.REJECTED) return <span>error</span>;
|
|
244
|
+
return <span>{JSON.stringify(wrapped.value)}</span>;
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
**Signature:**
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
function usePending<T>(
|
|
251
|
+
source$: ObservableLike<T>,
|
|
252
|
+
getDefaultValue?: () => T,
|
|
253
|
+
): IWrapped<T, StatusEnum.PENDING | StatusEnum.FULFILLED | StatusEnum.REJECTED>
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
> **Reload** is not part of the status object; trigger it via the `useStateData` handle's `reload()` or `getAttachedReload(source$)` from `rxfy`.
|
|
257
|
+
>
|
|
258
|
+
> **Contract:** `source$` must be referentially stable across renders (memoize it; `data$` from `useStateData` already is). A new identity restarts the pipeline from `PENDING`; an observable created inline in render restarts every render and never settles.
|
|
259
|
+
|
|
260
|
+
### `useObservable`
|
|
261
|
+
|
|
262
|
+
Low-level hook that subscribes to any `Observable<T>` via `useSyncExternalStore`.
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
import { useObservable } from "rxfy-react";
|
|
266
|
+
|
|
267
|
+
const value = useObservable(observable$, defaultValue);
|
|
268
|
+
const maybeValue = useObservable(observable$); // T | undefined
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
**Signature:**
|
|
272
|
+
|
|
273
|
+
```ts
|
|
274
|
+
function useObservable<T>(observable: Observable<T>, initialValue: T): T
|
|
275
|
+
function useObservable<T>(observable: Observable<T>): T | undefined
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Emissions that are deep-equal (`lodash.isEqual`) to the current value are skipped; re-emitting an identical value does not re-render.
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## Context / registry internals
|
|
283
|
+
|
|
284
|
+
`ModelRegistryContext` and `useModelRegistry` expose the underlying React context. Use them directly only when building a custom provider or accessing the registry outside `StoreProvider`.
|
|
285
|
+
|
|
286
|
+
```tsx
|
|
287
|
+
import { ModelRegistryContext, useModelRegistry } from "rxfy-react";
|
|
288
|
+
import { createModelRegistry } from "rxfy";
|
|
289
|
+
|
|
290
|
+
// custom provider
|
|
291
|
+
const registry = createModelRegistry();
|
|
292
|
+
<ModelRegistryContext.Provider value={registry}>
|
|
293
|
+
<App />
|
|
294
|
+
</ModelRegistryContext.Provider>
|
|
295
|
+
|
|
296
|
+
// inside any child; throws if no provider is found above
|
|
297
|
+
const registry = useModelRegistry();
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
**Signatures:**
|
|
22
301
|
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
302
|
+
```ts
|
|
303
|
+
const ModelRegistryContext: React.Context<IModelRegistry | null>
|
|
304
|
+
function useModelRegistry(): IModelRegistry
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
## Server-side rendering
|
|
310
|
+
|
|
311
|
+
SSR is on-demand: there is no prefetch API. Components declare their data with `useStateData` exactly as on the client; on the server (`<StoreProvider ssr>`) a cache miss suspends until the fetch settles. Results (fulfilled or rejected) are captured in the registry, serialized into the HTML, and ingested on the client so the first paint is already fulfilled: no loading flash, no re-fetch, no hydration mismatch.
|
|
312
|
+
|
|
313
|
+
Requirements: give models a `name` and states a `key` (stable string identities), and write `fetchFn` to work in both environments (it runs on the server during SSR and on the client for reloads).
|
|
314
|
+
|
|
315
|
+
### Buffered mode (any Node server)
|
|
316
|
+
|
|
317
|
+
Wait for every Suspense boundary with `onAllReady`, then send the complete document. This is the recommended non-Next mode; [examples/vite-todo](../../examples/vite-todo) runs it end to end.
|
|
318
|
+
|
|
319
|
+
```tsx
|
|
320
|
+
// server
|
|
321
|
+
import { renderToPipeableStream } from "react-dom/server";
|
|
322
|
+
import { createModelRegistry, dehydrate, hydrationScript } from "rxfy";
|
|
323
|
+
import { StoreProvider } from "rxfy-react";
|
|
324
|
+
|
|
325
|
+
function render(): Promise<{ html: string; state: string }> {
|
|
326
|
+
const registry = createModelRegistry(); // one per request
|
|
327
|
+
return new Promise((resolve, reject) => {
|
|
328
|
+
const { pipe } = renderToPipeableStream(
|
|
329
|
+
<StoreProvider registry={registry} ssr><App /></StoreProvider>,
|
|
330
|
+
{
|
|
331
|
+
onAllReady() {
|
|
332
|
+
// collect the stream into a string, then:
|
|
333
|
+
// resolve({ html, state: hydrationScript(dehydrate(registry)) });
|
|
334
|
+
},
|
|
335
|
+
onError: reject,
|
|
336
|
+
},
|
|
337
|
+
);
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// template: <div id="root"><!--app-html--></div><!--app-state-->
|
|
342
|
+
// inject: template.replace("<!--app-state-->", state)
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
```tsx
|
|
346
|
+
// client: StoreProvider picks the snapshot up from the injected script automatically
|
|
347
|
+
import { hydrateRoot } from "react-dom/client";
|
|
348
|
+
|
|
349
|
+
hydrateRoot(
|
|
350
|
+
document.getElementById("root")!,
|
|
351
|
+
<StoreProvider ssr><App /></StoreProvider>,
|
|
352
|
+
);
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### Streaming mode (Next.js App Router)
|
|
27
356
|
|
|
28
|
-
|
|
29
|
-
|
|
357
|
+
`rxfy-react/next` ships `<HydrationStream />`: on each stream flush it emits newly settled queries and newly written entities as `window.__RXFY_SSR__.push(...)` script tags; the client `StoreProvider` ingests them, including chunks arriving after hydration starts.
|
|
358
|
+
|
|
359
|
+
```tsx
|
|
360
|
+
// app/providers.tsx
|
|
361
|
+
"use client";
|
|
362
|
+
import { StoreProvider } from "rxfy-react";
|
|
363
|
+
import { HydrationStream } from "rxfy-react/next";
|
|
364
|
+
|
|
365
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
|
30
366
|
return (
|
|
31
|
-
<
|
|
32
|
-
|
|
33
|
-
|
|
367
|
+
<StoreProvider ssr>
|
|
368
|
+
<HydrationStream />
|
|
369
|
+
{children}
|
|
370
|
+
</StoreProvider>
|
|
34
371
|
);
|
|
35
372
|
}
|
|
36
373
|
```
|
|
374
|
+
|
|
375
|
+
`next` is an optional peer dependency; only this subpath needs it.
|
|
376
|
+
|
|
377
|
+
### Two-pass mode (strict `renderToString`)
|
|
378
|
+
|
|
379
|
+
For environments without React stream APIs, `collectStateData` loops render passes until nothing suspends (the `getDataFromTree` pattern; each fetch waterfall level costs one extra pass):
|
|
380
|
+
|
|
381
|
+
```ts
|
|
382
|
+
import { renderToString } from "react-dom/server";
|
|
383
|
+
import { collectStateData } from "rxfy-react";
|
|
384
|
+
|
|
385
|
+
const html = await collectStateData(registry, () =>
|
|
386
|
+
renderToString(<StoreProvider registry={registry} ssr><App /></StoreProvider>),
|
|
387
|
+
);
|
|
388
|
+
const state = hydrationScript(dehydrate(registry));
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
**Signature:**
|
|
392
|
+
|
|
393
|
+
```ts
|
|
394
|
+
function collectStateData(registry: IModelRegistry, render: () => string): Promise<string>
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### Error handling
|
|
398
|
+
|
|
399
|
+
A `fetchFn` rejection on the server is captured as a serialized rejected entry (`{ name, message }`, stack stripped) and hydrates as rejected state: the server HTML shows your `<Pending rejected>` UI, and the handle's `reload()` retries client-side with a real fetch.
|
|
400
|
+
|
|
401
|
+
---
|
|
402
|
+
|
|
403
|
+
## See also
|
|
404
|
+
|
|
405
|
+
- [rxfy: Core API](../rxfy/README.md)
|
|
406
|
+
- [examples/vite-todo](../../examples/vite-todo): full working example with buffered SSR and URL-driven state
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
9
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
20
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
21
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
22
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
23
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
24
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
25
|
+
mod
|
|
26
|
+
));
|
|
27
|
+
|
|
28
|
+
// src/registry-context.ts
|
|
29
|
+
import { createContext, useContext } from "react";
|
|
30
|
+
var ModelRegistryContext = createContext(null);
|
|
31
|
+
function useModelRegistry() {
|
|
32
|
+
const ctx = useContext(ModelRegistryContext);
|
|
33
|
+
if (!ctx) throw new Error("StoreProvider not found");
|
|
34
|
+
return ctx;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export {
|
|
38
|
+
__commonJS,
|
|
39
|
+
__toESM,
|
|
40
|
+
ModelRegistryContext,
|
|
41
|
+
useModelRegistry
|
|
42
|
+
};
|