@jsenv/navi 0.29.19 → 0.29.21
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/jsenv_navi.js +703 -62
- package/dist/jsenv_navi.js.map +92 -35
- package/docs/AI_INSTRUCTIONS.md +4 -0
- package/docs/actions.md +117 -250
- package/docs/list_refresh.md +131 -0
- package/docs/resource.md +90 -0
- package/docs/scroll.md +1 -1
- package/package.json +1 -1
package/docs/AI_INSTRUCTIONS.md
CHANGED
|
@@ -33,6 +33,10 @@ consistency across the app, not from any single call site.
|
|
|
33
33
|
- `docs/resource.md` — REST state: `resource()`, relationships, callback return
|
|
34
34
|
contracts, autorerun rules. Companion files: `docs/actions.md`,
|
|
35
35
|
`docs/resource_with_params.md`, `docs/resource_dependencies.md`.
|
|
36
|
+
- `docs/list_refresh.md` — what a write sends back to the network and what stays
|
|
37
|
+
on screen meanwhile: stale data returned by `useAsyncData({ loading: true })`,
|
|
38
|
+
what updates from a response without any request, `rerunOn` and its defaults.
|
|
39
|
+
Read it before adding verbs to `rerunOn` or hiding a list on `loading`.
|
|
36
40
|
- `docs/css_architecture.md` — how Navi's CSS layering works, and the
|
|
37
41
|
supported ways to override component styles (props > CSS variables > direct
|
|
38
42
|
rule overrides, in that preference order).
|
package/docs/actions.md
CHANGED
|
@@ -1,250 +1,117 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
-
|
|
114
|
-
|
|
115
|
-
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
-
|
|
121
|
-
- ## Usage Patterns
|
|
122
|
-
-
|
|
123
|
-
- ### 🏗️ **Basic Resource Loading**
|
|
124
|
-
- ```js
|
|
125
|
-
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
- const getUserAction = createActionTemplate(async ({ userId }) => {
|
|
129
|
-
- return await api.getUser(userId);
|
|
130
|
-
- });
|
|
131
|
-
-
|
|
132
|
-
- // In component
|
|
133
|
-
- const userAction = getUserAction.instantiate({ userId: 123 });
|
|
134
|
-
- const { pending, data, error } = useActionStatus(userAction);
|
|
135
|
-
-
|
|
136
|
-
- useEffect(() => {
|
|
137
|
-
- userAction.load();
|
|
138
|
-
- }, []);
|
|
139
|
-
- ```
|
|
140
|
-
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
-
|
|
144
|
-
- ### 🔄 **Reactive Data Loading**
|
|
145
|
-
- ```js
|
|
146
|
-
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
- const searchProxy = createActionProxy(searchTemplate, {
|
|
150
|
-
- query: searchSignal,
|
|
151
|
-
- filters: filtersSignal
|
|
152
|
-
- });
|
|
153
|
-
- // Automatically reloads when signals change
|
|
154
|
-
- ```
|
|
155
|
-
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
-
|
|
159
|
-
- ### 📋 **Master-Detail Pattern**
|
|
160
|
-
- ```js
|
|
161
|
-
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
- const usersAction = getUsersTemplate.instantiate();
|
|
165
|
-
- const selectedUser = signal(null);
|
|
166
|
-
-
|
|
167
|
-
- const userDetailsProxy = createActionProxy(getUserTemplate, {
|
|
168
|
-
- userId: computed(() => selectedUser.value?.id)
|
|
169
|
-
- });
|
|
170
|
-
- ```
|
|
171
|
-
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
-
|
|
175
|
-
- ### 🏃 **Progressive Loading**
|
|
176
|
-
- ```js
|
|
177
|
-
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
- // Preload on hover, load on click
|
|
181
|
-
- <button
|
|
182
|
-
- onMouseEnter={() => action.preload()}
|
|
183
|
-
- onClick={() => action.load()}
|
|
184
|
-
- >
|
|
185
|
-
- Load User
|
|
186
|
-
- </button>
|
|
187
|
-
- ```
|
|
188
|
-
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
-
|
|
192
|
-
- ## Advanced Features
|
|
193
|
-
-
|
|
194
|
-
- ### 🎭 **Custom Data Transformation**
|
|
195
|
-
- ```js
|
|
196
|
-
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
- const actionTemplate = createActionTemplate(fetchUser, {
|
|
200
|
-
- computedDataSignal: computed(() => {
|
|
201
|
-
- const rawData = dataSignal.value;
|
|
202
|
-
- return rawData ? transformUser(rawData) : null;
|
|
203
|
-
- })
|
|
204
|
-
- });
|
|
205
|
-
- ```
|
|
206
|
-
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
-
|
|
210
|
-
- ### 🎨 **Async Rendering Support**
|
|
211
|
-
- ```js
|
|
212
|
-
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
- const actionTemplate = createActionTemplate(fetchData, {
|
|
216
|
-
- renderLoadedAsync: async () => {
|
|
217
|
-
- const { UserComponent } = await import('./UserComponent.js');
|
|
218
|
-
- return (user) => <UserComponent user={user} />;
|
|
219
|
-
- }
|
|
220
|
-
- });
|
|
221
|
-
- ```
|
|
222
|
-
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
-
|
|
226
|
-
- ### 🛠️ **Debugging & Observability**
|
|
227
|
-
- Built-in debug mode with detailed logging of state transitions, loading coordination,
|
|
228
|
-
- and memory management. Enable with `debug = true`.
|
|
229
|
-
-
|
|
230
|
-
- ## Integration Points
|
|
231
|
-
-
|
|
232
|
-
- - **Signals**: Built on @preact/signals for reactive state management
|
|
233
|
-
- - **Navigation**: Integrates with navigation systems for route-based loading
|
|
234
|
-
- - **Components**: Use `useActionStatus()` hook for component integration
|
|
235
|
-
- - **Memory Management**: Automatic cleanup with WeakMap-based private properties
|
|
236
|
-
-
|
|
237
|
-
- ## Performance Characteristics
|
|
238
|
-
-
|
|
239
|
-
- - **Memory Efficient**: Weak references prevent memory leaks
|
|
240
|
-
- - **Request Deduplication**: Identical requests are automatically merged
|
|
241
|
-
- - **Minimal Re-renders**: Signal-based updates only trigger when data actually changes
|
|
242
|
-
- - **Lazy Loading**: Actions only created when needed, with intelligent memoization
|
|
243
|
-
-
|
|
244
|
-
- This system is particularly well-suited for:
|
|
245
|
-
- - SPAs with complex data fetching requirements
|
|
246
|
-
- - Applications needing fine-grained loading state control
|
|
247
|
-
- - Systems requiring request coordination and deduplication
|
|
248
|
-
- - Progressive loading and preloading scenarios
|
|
249
|
-
- - Master-detail interfaces with dynamic parameter binding
|
|
250
|
-
\*/
|
|
1
|
+
# Actions
|
|
2
|
+
|
|
3
|
+
An action is an async callback plus the state of its last run, held in signals:
|
|
4
|
+
running or not, the error it failed with, the data it produced. Components read
|
|
5
|
+
that state instead of keeping their own.
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
import { createAction } from "@jsenv/navi";
|
|
9
|
+
|
|
10
|
+
const getUser = createAction(async ({ id }, { signal }) => {
|
|
11
|
+
const response = await fetch(`/users/${id}`, { signal });
|
|
12
|
+
return response.json();
|
|
13
|
+
});
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The callback receives `(params, { reason, event, signal, isPrerun })`. `signal`
|
|
17
|
+
is aborted when the run is called off — pass it to `fetch`.
|
|
18
|
+
|
|
19
|
+
`resource()` creates one action per REST callback rather than having you write
|
|
20
|
+
them by hand — see [resource.md](./resource.md).
|
|
21
|
+
|
|
22
|
+
## Params: `bindParams`, and calling the action
|
|
23
|
+
|
|
24
|
+
`createAction` gives one action for the callback; the params make instances of
|
|
25
|
+
it, each with its own state:
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
const getUser123 = getUser.bindParams({ id: 123 });
|
|
29
|
+
await getUser123.run();
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Two `bindParams` with equal params give **the same instance** (deep equality),
|
|
33
|
+
which is what makes state shared between two components asking for the same
|
|
34
|
+
thing, and what deduplicates their requests.
|
|
35
|
+
|
|
36
|
+
An action is callable, and calling it is the short way to bind and run in one
|
|
37
|
+
go:
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
getUser({ id: 123 }); // getUser.bindParams({ id: 123 }).rerun()
|
|
41
|
+
getUser(); // getUser.rerun()
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Use it wherever a run is a **gesture** — a click handler, an event, a step in a
|
|
45
|
+
flow — where the params are known at that moment and the run is the point:
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
const deleteGame = (game) => GAME.DELETE({ id: game.id });
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Use `bindParams` when what you need is the **instance**, not the run: to hand it
|
|
52
|
+
to a component that will run it and read its state
|
|
53
|
+
(`<Button action={GAME.DELETE.bindParams({ id })}>`), or to keep a handle on it.
|
|
54
|
+
Note that calling the action `rerun()`s it — the run happens even if that
|
|
55
|
+
instance already holds data, which is what you want from a gesture and not what
|
|
56
|
+
you want from a component asking for data.
|
|
57
|
+
|
|
58
|
+
Params may be signals, and then the action follows them:
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
const userAction = getUser.bindParams({ id: userIdSignal });
|
|
62
|
+
// a new params value reruns it
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Running: `run`, `rerun`, `prerun`, `reset`
|
|
66
|
+
|
|
67
|
+
| Method | Does |
|
|
68
|
+
| ---------- | ------------------------------------------------------------------------ |
|
|
69
|
+
| `run()` | Asks for the data. An action already running or completed has it: no-op. |
|
|
70
|
+
| `rerun()` | Runs again whatever state it is in — a refresh, an explicit "check now". |
|
|
71
|
+
| `prerun()` | Same as `run()`, in the background: nothing asked for it on screen yet. |
|
|
72
|
+
| `reset()` | Aborts what is running and puts the action back to idle, data and all. |
|
|
73
|
+
| `abort()` | Calls off the run in flight, keeping the data it had. |
|
|
74
|
+
|
|
75
|
+
## Reading an action
|
|
76
|
+
|
|
77
|
+
```jsx
|
|
78
|
+
const [user] = useAsyncData(userAction);
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`useAsyncData` suspends until the data is there and throws on failure, leaving
|
|
82
|
+
both to the nearest `<Loading>` and `<ErrorBoundary>`; pass `{ loading: true }`
|
|
83
|
+
or `{ error: true }` to handle either inside the component (stale data stays
|
|
84
|
+
available while a rerun is in flight). `useActionStatus(action)` gives the whole
|
|
85
|
+
state at once — `{ idle, loading, completed, aborted, error, data, params }` —
|
|
86
|
+
for a component that needs to look at it rather than render it.
|
|
87
|
+
|
|
88
|
+
Controls take the action itself and wire the rest: `<Button action>` runs it on
|
|
89
|
+
click, shows its loading state, and puts its error where the user can see it.
|
|
90
|
+
That is the reason to pass an action instance rather than an arrow calling the
|
|
91
|
+
callback:
|
|
92
|
+
|
|
93
|
+
```jsx
|
|
94
|
+
// ✓ loading, error and disabled states come from the action
|
|
95
|
+
<Button action={GAME_CANDIDATES.POST.bindParams({ id: game.id, user_id })}>
|
|
96
|
+
Accept
|
|
97
|
+
</Button>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Reruns
|
|
101
|
+
|
|
102
|
+
Actions do not stay stale on their own: a resource's `POST` reruns the
|
|
103
|
+
`GET_MANY` that lists it, a `DELETE` resets the `GET` that loaded the item, and
|
|
104
|
+
`dependencies`/`rerunOn` declare the rest. What re-runs after a write, and what
|
|
105
|
+
stays on screen while it does, is in [list_refresh.md](./list_refresh.md) and
|
|
106
|
+
[resource_dependencies.md](./resource_dependencies.md).
|
|
107
|
+
|
|
108
|
+
`rerunActions(actions)` / `updateActions(actions)` drive several at once (route
|
|
109
|
+
changes do exactly that).
|
|
110
|
+
|
|
111
|
+
## See also
|
|
112
|
+
|
|
113
|
+
- [resource.md](./resource.md) — actions created from REST callbacks, and the
|
|
114
|
+
store behind them
|
|
115
|
+
- [resource_with_params.md](./resource_with_params.md) — `withParams()` and
|
|
116
|
+
isolated rerun scopes
|
|
117
|
+
- [list_refresh.md](./list_refresh.md) — what a write refreshes
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# A list while it refreshes
|
|
2
|
+
|
|
3
|
+
When a write touches one item of a list, two questions decide what the user
|
|
4
|
+
sees: **what goes back to the network**, and **what stays on screen meanwhile**.
|
|
5
|
+
Getting either wrong turns "pause one row" into a full page reload.
|
|
6
|
+
|
|
7
|
+
The short answer:
|
|
8
|
+
|
|
9
|
+
- A write that returns the modified item fixes every list containing it, with
|
|
10
|
+
no request and no loading state at all.
|
|
11
|
+
- `useAsyncData(action, { loading: true })` keeps returning the previous data
|
|
12
|
+
while the action re-runs — the list is never taken away unless the component
|
|
13
|
+
throws it away.
|
|
14
|
+
|
|
15
|
+
## `loading: true` returns the previous value
|
|
16
|
+
|
|
17
|
+
`useAsyncData` returns `[data, loading, error]`. During a re-run, `data` is the
|
|
18
|
+
**previous data**, not `undefined`:
|
|
19
|
+
|
|
20
|
+
| Moment | `data` | `loading` |
|
|
21
|
+
| --------------------- | ----------- | --------- |
|
|
22
|
+
| never completed yet | `undefined` | `true` |
|
|
23
|
+
| running after success | previous | `true` |
|
|
24
|
+
| completed | fresh | `false` |
|
|
25
|
+
|
|
26
|
+
So the emptiness test is `data === undefined`, never `loading`:
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
const [items, loading] = useAsyncData(ACTION, { loading: true });
|
|
30
|
+
if (items === undefined) {
|
|
31
|
+
return null; // first load: there is nothing to show yet
|
|
32
|
+
}
|
|
33
|
+
return <RadarList radars={items} busy={loading} />; // re-read: stay on screen
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
// ✗ blanks the page on every re-run, for a checkbox on one row
|
|
38
|
+
if (loading) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Read `loading` as "what you are displaying is from before", not "there is
|
|
44
|
+
nothing to display".
|
|
45
|
+
|
|
46
|
+
`<List loading>` is the first-load answer, not the refresh one: it replaces the
|
|
47
|
+
rows with skeletons. Pass it while stale rows exist and they disappear — same
|
|
48
|
+
mistake as `loading ? null :`, one level down.
|
|
49
|
+
|
|
50
|
+
## What updates without a request
|
|
51
|
+
|
|
52
|
+
An action's `data` is a computation over the resource store, not a snapshot of
|
|
53
|
+
its own last response. Any write that puts an item into the store recomputes
|
|
54
|
+
every list holding it, while the `GET_MANY` action stays `COMPLETED` — no
|
|
55
|
+
request, no loading state, no flicker.
|
|
56
|
+
|
|
57
|
+
The condition is the whole hinge of the system:
|
|
58
|
+
|
|
59
|
+
> **the write's callback must return the item, with its key** — `{ id, … }`.
|
|
60
|
+
|
|
61
|
+
Partial props are fine (`{ id, paused: true }` merges into the stored item); the
|
|
62
|
+
key is what cannot be missing. The corollary matters just as much: a callback
|
|
63
|
+
that returns nothing — a `204`, or a `fetch` whose result is dropped — writes
|
|
64
|
+
nothing to the store, and the change never reaches the screen. There, the only
|
|
65
|
+
way back is a re-read.
|
|
66
|
+
|
|
67
|
+
`DELETE` is symmetric: returning the id drops the item from the store, and every
|
|
68
|
+
list containing it drops it too.
|
|
69
|
+
|
|
70
|
+
## `rerunOn`, verb by verb
|
|
71
|
+
|
|
72
|
+
`rerunOn` says which verbs invalidate this resource's `GET` / `GET_MANY`:
|
|
73
|
+
|
|
74
|
+
```js
|
|
75
|
+
const GAME_RADAR = resource("game_radar", {
|
|
76
|
+
rerunOn: { GET_MANY: ["POST", "DELETE"] },
|
|
77
|
+
…
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Defaults are `{ GET: false, GET_MANY: ["POST"] }`:
|
|
82
|
+
|
|
83
|
+
| Default | Why |
|
|
84
|
+
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
85
|
+
| `GET: false` | `PUT`/`PATCH` already update the UI through the store; `DELETE` resets the `GET` rather than re-running it, so a deleted item shows nothing instead of a spinner then a 404. Give the deleted case its own UI (an "item not found" panel, a redirect) instead of `GET: ["DELETE"]`. |
|
|
86
|
+
| `GET_MANY: ["POST"]` | Whether a new item belongs in this list depends on filters, pagination, sort — the backend knows, the client does not. `DELETE` is excluded because the store already removes the item from every list. |
|
|
87
|
+
|
|
88
|
+
Adding `PUT`/`PATCH` to `GET_MANY` is the usual over-correction: it costs a
|
|
89
|
+
request and a `loading` pass to obtain something the response already contained.
|
|
90
|
+
When a `PATCH` "doesn't show", the fix is almost always server-side — return the
|
|
91
|
+
updated item — not client-side refreshing.
|
|
92
|
+
|
|
93
|
+
## Decision table
|
|
94
|
+
|
|
95
|
+
| What changed | Re-read the list? |
|
|
96
|
+
| ------------------------- | ---------------------------------------------------- |
|
|
97
|
+
| a field of one item | no — the write's response is enough |
|
|
98
|
+
| membership of the list | yes (`POST`) — the backend decides who belongs |
|
|
99
|
+
| the ORDER of the list | yes — the store stores, it does not sort (see below) |
|
|
100
|
+
| nothing came back (`204`) | yes — there is nothing to put in the store |
|
|
101
|
+
|
|
102
|
+
## The store stores, it does not sort
|
|
103
|
+
|
|
104
|
+
Each `*_MANY` action holds its own array of ids; its data is those ids resolved
|
|
105
|
+
against the store, in that order. A `PUT_MANY` returning the collection in a new
|
|
106
|
+
order updates every item, and sets the order **of the `PUT_MANY` action** — the
|
|
107
|
+
`GET_MANY` list keeps the order it already had.
|
|
108
|
+
|
|
109
|
+
That is the design (a store is not an index), but it has a sharp edge: a
|
|
110
|
+
drag-and-drop that "works" on screen — because local state holds the order —
|
|
111
|
+
comes back in the old order on the next visit. After a reorder, re-read the list
|
|
112
|
+
explicitly:
|
|
113
|
+
|
|
114
|
+
```js
|
|
115
|
+
await REORDER_ACTION.run({ … });
|
|
116
|
+
GET_MANY_ACTION.rerun();
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## `.rerun()`, not `.run()`, to refresh
|
|
120
|
+
|
|
121
|
+
`.run()` on an action that already `COMPLETED` does nothing: it is a request to
|
|
122
|
+
have the data, and the data is there. Wiring a "check now" button to `.run()`
|
|
123
|
+
therefore checks nothing, silently. Use `.rerun()`, which resets the action and
|
|
124
|
+
runs it again.
|
|
125
|
+
|
|
126
|
+
## See also
|
|
127
|
+
|
|
128
|
+
- [resource.md](./resource.md) — `resource()`, relations, callback return
|
|
129
|
+
contracts
|
|
130
|
+
- [resource_dependencies.md](./resource_dependencies.md) — invalidating a
|
|
131
|
+
resource from another one
|
package/docs/resource.md
CHANGED
|
@@ -25,10 +25,94 @@ Each callback returns the data to upsert into the store:
|
|
|
25
25
|
| GET / POST / PUT / PATCH | the full item object, `{ id, … }` |
|
|
26
26
|
| DELETE | the id, or `{ id }` |
|
|
27
27
|
| GET_MANY / POST_MANY / … | an array of item objects |
|
|
28
|
+
| GET_PAGE | `{ items, start, count }` (below) |
|
|
28
29
|
|
|
29
30
|
Actions are read in components through the action system (`useAsyncData`,
|
|
30
31
|
`<Button action>`, …) — see [actions.md](./actions.md).
|
|
31
32
|
|
|
33
|
+
## `store.upsert()` is not how data enters the store
|
|
34
|
+
|
|
35
|
+
navi writes the store. Declare the resource and its relations, return the shape
|
|
36
|
+
each callback owes (tables above and below), and the write happens. A hand
|
|
37
|
+
written `store.upsert()` in the path of a normal request of the resource is the
|
|
38
|
+
sign that something is missing: a relation that is not declared, or a callback
|
|
39
|
+
that does not return what it should.
|
|
40
|
+
|
|
41
|
+
`store` is exposed for what is **not** a request of the resource, where writing
|
|
42
|
+
it yourself is the point:
|
|
43
|
+
|
|
44
|
+
- seeding it with data that came from elsewhere — state rendered by the server,
|
|
45
|
+
a local cache, a websocket message;
|
|
46
|
+
- absorbing the parent object a relation route answered with: the relation
|
|
47
|
+
callback only writes the relation, so the parent's own fields have nowhere
|
|
48
|
+
else to land (see [When the backend answers a sub-route with the whole
|
|
49
|
+
parent](#when-the-backend-answers-a-sub-route-with-the-whole-parent)).
|
|
50
|
+
|
|
51
|
+
A list that loads its rows page by page is **not** one of those cases — that is
|
|
52
|
+
`GET_PAGE`, right below.
|
|
53
|
+
|
|
54
|
+
## `GET_PAGE`: feeding a list that loads as it scrolls
|
|
55
|
+
|
|
56
|
+
A `<List.Items>` asks for the rows it is about to draw and keeps what it gets.
|
|
57
|
+
`GET_PAGE` is the resource's answer to that question — one slice at a time:
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
const GAME = resource("game", {
|
|
61
|
+
GET: ({ id }) => fetchJson(`/games/${id}`),
|
|
62
|
+
GET_PAGE: ({ radar, start, limit }) =>
|
|
63
|
+
fetchJson(`/radars/${radar.id}/games?start=${start}&limit=${limit}`),
|
|
64
|
+
// { items: [{ id, … }, …], start: 20, count: 137 }
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```jsx
|
|
69
|
+
<List.Items
|
|
70
|
+
count={radar.match_count}
|
|
71
|
+
itemsAction={GAME.GET_PAGE.bindParams({ radar })}
|
|
72
|
+
renderItem={(game) => <GameCard game={game} />}
|
|
73
|
+
/>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The callback receives the bound params merged with the range the list asks for
|
|
77
|
+
(`start`, `end`, `limit`, `before`, `after`, `around`, `count`), and a `signal`
|
|
78
|
+
as second argument — aborted when the list stops wanting those rows. It returns
|
|
79
|
+
a page the way a `Content-Range` does: **`{ items, start, count }`** — these
|
|
80
|
+
rows, at this place, out of that many. `start` may be omitted when the list
|
|
81
|
+
asked for a positive one; `count` defaults to `start + items.length` (a source
|
|
82
|
+
that does not know its total). The items are upserted on their way in, so the
|
|
83
|
+
list draws store items — never copies of the JSON — and a request sent from a
|
|
84
|
+
row is read back on that row.
|
|
85
|
+
|
|
86
|
+
A row that must follow its **own fields** through a write reads them from the
|
|
87
|
+
store rather than from the object it was handed:
|
|
88
|
+
|
|
89
|
+
```jsx
|
|
90
|
+
const GameCard = ({ id }) => {
|
|
91
|
+
const game = GAME.useById(id); // the item as it is now
|
|
92
|
+
…
|
|
93
|
+
};
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
An update replaces the item object (the store holds values, and that is what
|
|
97
|
+
makes a change detectable), so the one the list is holding is the one it was
|
|
98
|
+
given. Relations are not concerned: they are keyed by owner, and a row reading
|
|
99
|
+
`game.candidates` reads the shared collection whatever object carries it.
|
|
100
|
+
|
|
101
|
+
`GET_PAGE` is a **reader, not an action**. It keeps no value and takes no place
|
|
102
|
+
in the rerun graph, which is what makes it usable per slice:
|
|
103
|
+
|
|
104
|
+
- the list already holds the pages it received and glues them back together —
|
|
105
|
+
a second memory holding one of them would fight it;
|
|
106
|
+
- a `POST` invalidating "the collection" would otherwise send every page ever
|
|
107
|
+
loaded back to the network at once.
|
|
108
|
+
|
|
109
|
+
What it does not give is membership: an item that leaves the collection stays on
|
|
110
|
+
screen until the rows are asked for again. Give the screen its own way to ask
|
|
111
|
+
(a refresh gesture, a `key` on the run).
|
|
112
|
+
|
|
113
|
+
It reads a collection, so it lives on the resource itself (or on a
|
|
114
|
+
`withParams()` of it), not on a relation.
|
|
115
|
+
|
|
32
116
|
## Relations: pick one of the four methods
|
|
33
117
|
|
|
34
118
|
A backend sub-route (`/games/:id/candidates`, `/games/:id/candidates/:userId/seen`)
|
|
@@ -258,10 +342,16 @@ The arrow works, but nothing tracks it: no per-row spinner, no error surfaced on
|
|
|
258
342
|
the button that caused it, no deduplication of concurrent runs, no autorerun of
|
|
259
343
|
the actions this mutation should invalidate. See [actions.md](./actions.md).
|
|
260
344
|
|
|
345
|
+
Away from a component, where the run is a gesture and not something to render,
|
|
346
|
+
an action is callable: `GAME.DELETE({ id })` is
|
|
347
|
+
`GAME.DELETE.bindParams({ id }).rerun()`.
|
|
348
|
+
|
|
261
349
|
## See also
|
|
262
350
|
|
|
263
351
|
- [resource_with_params.md](./resource_with_params.md) — `withParams()` and
|
|
264
352
|
isolated lifecycles
|
|
265
353
|
- [resource_dependencies.md](./resource_dependencies.md) — cross-resource
|
|
266
354
|
autorerun
|
|
355
|
+
- [list_refresh.md](./list_refresh.md) — what re-runs after a write, and what
|
|
356
|
+
stays on screen while it does
|
|
267
357
|
- [actions.md](./actions.md) — action lifecycle, `bindParams`, `useAsyncData`
|
package/docs/scroll.md
CHANGED
|
@@ -229,7 +229,7 @@ is a trade, not a leak.
|
|
|
229
229
|
own body) but never grows on its own. Growing is the caller's decision —
|
|
230
230
|
`expandY`.
|
|
231
231
|
|
|
232
|
-
Pass `
|
|
232
|
+
Pass `travelByKeyboard={false}` when the arrow keys belong to the content (a list
|
|
233
233
|
one walks through, a picker whose slides are steps): otherwise the right arrow
|
|
234
234
|
changes screen mid-reading.
|
|
235
235
|
|