@jsenv/navi 0.29.19 → 0.29.20

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.
@@ -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).
@@ -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
@@ -264,4 +264,6 @@ the actions this mutation should invalidate. See [actions.md](./actions.md).
264
264
  isolated lifecycles
265
265
  - [resource_dependencies.md](./resource_dependencies.md) — cross-resource
266
266
  autorerun
267
+ - [list_refresh.md](./list_refresh.md) — what re-runs after a write, and what
268
+ stays on screen while it does
267
269
  - [actions.md](./actions.md) — action lifecycle, `bindParams`, `useAsyncData`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/navi",
3
- "version": "0.29.19",
3
+ "version": "0.29.20",
4
4
  "type": "module",
5
5
  "description": "Library of components including navigation to create frontend applications",
6
6
  "repository": {