@jsenv/navi 0.29.166 → 0.29.167
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 +30 -10
- package/dist/jsenv_navi.js.map +3 -3
- package/docs/AI_INSTRUCTIONS.md +11 -0
- package/docs/actions.md +6 -4
- package/docs/data_states.md +147 -0
- package/docs/error_handling.md +9 -0
- package/docs/list_refresh.md +16 -1
- package/package.json +1 -1
package/docs/AI_INSTRUCTIONS.md
CHANGED
|
@@ -69,6 +69,17 @@ consistency across the app, not from any single call site.
|
|
|
69
69
|
collection (searching it is that collection's own `GET_MANY`). Companion
|
|
70
70
|
files: `docs/actions.md`, `docs/resource_with_params.md`,
|
|
71
71
|
`docs/resource_dependencies.md`.
|
|
72
|
+
- `docs/data_states.md` — what a screen draws while its data is loading,
|
|
73
|
+
missing, stale or failed: the three questions `data`, `loading` and `error`
|
|
74
|
+
answer separately, the four combinations of the first two and what each one
|
|
75
|
+
means, and the invariant that `loading: true` never suspends — so a component
|
|
76
|
+
drawing its own states is never taken away, with the dialog, the scroll
|
|
77
|
+
position and the retry button it was holding. Also why a skeleton is TOLD
|
|
78
|
+
whether it is loading rather than deducing it from emptiness, why an error
|
|
79
|
+
arrives beside the data rather than in place of it, and what `dismissError()`
|
|
80
|
+
does and does not do. Read it before drawing a skeleton on `!data`, before
|
|
81
|
+
hiding content because `error` is set, and before adding a state flag of your
|
|
82
|
+
own next to what the hook already returns.
|
|
72
83
|
- `docs/list_refresh.md` — what a write sends back to the network and what stays
|
|
73
84
|
on screen meanwhile: stale data returned by `useAsyncData({ loading: true })`,
|
|
74
85
|
what updates from a response without any request, `rerunOn` and its defaults,
|
package/docs/actions.md
CHANGED
|
@@ -130,10 +130,12 @@ const [user] = useAsyncData(userAction);
|
|
|
130
130
|
|
|
131
131
|
`useAsyncData` suspends until the data is there and throws on failure, leaving
|
|
132
132
|
both to the nearest `<Loading>` and `<ErrorBoundary>`; pass `{ loading: true }`
|
|
133
|
-
or `{ error: true }` to
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
[
|
|
133
|
+
or `{ error: true }` to receive either as a value instead, the component staying
|
|
134
|
+
mounted throughout and keeping the data it already had. What it then draws for
|
|
135
|
+
each state — the four combinations of `data` and `loading`, why a skeleton is
|
|
136
|
+
told whether it is loading — is [data_states.md](./data_states.md). Where a
|
|
137
|
+
failure goes when no screen takes it — and why a run settles with its error
|
|
138
|
+
rather than rejecting — is [error_handling.md](./error_handling.md). `useActionStatus(action)` gives the whole
|
|
137
139
|
state at once — `{ idle, loading, completed, aborted, error, data, params }` —
|
|
138
140
|
for a component that needs to look at it rather than render it. It reads that
|
|
139
141
|
very instance: to know what moves when a control is handed the action, see
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# The states of a screen's data
|
|
2
|
+
|
|
3
|
+
A screen reading async data draws one of a handful of states, and there are two
|
|
4
|
+
ways to get it wrong. Both are common, and both come from the same shortcut.
|
|
5
|
+
|
|
6
|
+
- **A wait announced over something that is not coming.** A skeleton drawn on
|
|
7
|
+
"there is no data" alone keeps shimmering after the load failed, and after the
|
|
8
|
+
params turned out to be false. It says "hold on", and there is nothing to hold
|
|
9
|
+
on for.
|
|
10
|
+
- **Content taken away because something behind it went wrong.** A list that
|
|
11
|
+
disappears because its refresh failed; a page replaced by a message while the
|
|
12
|
+
rows it had are still perfectly good, and still true a second ago.
|
|
13
|
+
|
|
14
|
+
The shortcut behind both: collapsing "loading", "empty" and "failed" into one
|
|
15
|
+
state — _not ready_ — and drawing one thing for it. They are three separate
|
|
16
|
+
questions, and a screen that draws honestly answers them separately:
|
|
17
|
+
|
|
18
|
+
- **is there something to show?** → `data`
|
|
19
|
+
- **is anything on its way?** → `loading`
|
|
20
|
+
- **did the last attempt fail?** → `error`
|
|
21
|
+
|
|
22
|
+
navi's part is to answer all three, at every render, and to keep answering them.
|
|
23
|
+
What is drawn for each is the component's, always: navi has no opinion on
|
|
24
|
+
whether a missing list is a skeleton, a sentence or an illustration.
|
|
25
|
+
|
|
26
|
+
## Asking for a state means receiving it as a value
|
|
27
|
+
|
|
28
|
+
Without options, `useAsyncData` **delegates**: it suspends while the data is not
|
|
29
|
+
there and throws when the action failed, so the states are drawn by the
|
|
30
|
+
`<Loading>` and `<ErrorBoundary>` above rather than by the component.
|
|
31
|
+
|
|
32
|
+
Each option turns one of those from something done _to_ the component into a
|
|
33
|
+
value the component reads:
|
|
34
|
+
|
|
35
|
+
| passed | the wait | the failure |
|
|
36
|
+
| --------------- | ------------------------- | ---------------------------- |
|
|
37
|
+
| nothing | suspends into `<Loading>` | throws to `<ErrorBoundary>` |
|
|
38
|
+
| `loading: true` | `loading`, a boolean | throws |
|
|
39
|
+
| `error: true` | suspends | `error`, plus `dismissError` |
|
|
40
|
+
| both | `loading` | `error` |
|
|
41
|
+
|
|
42
|
+
The invariant that makes this worth relying on: **`loading: true` never
|
|
43
|
+
suspends.** Not while the action runs, not while a debounced binding is
|
|
44
|
+
settling, not while it holds params nobody has started yet, not after a failure
|
|
45
|
+
whose message was dismissed. Every one of those comes back as a value.
|
|
46
|
+
|
|
47
|
+
That is not a detail of the wait — it is what keeps the component alive.
|
|
48
|
+
Suspending does not draw a spinner over a subtree: it takes the subtree away and
|
|
49
|
+
puts the boundary's fallback in its place, and everything under it is remounted
|
|
50
|
+
when it comes back. A dialog open inside it closes, a scroll position is lost,
|
|
51
|
+
half-typed text is gone, and the retry button the component was offering
|
|
52
|
+
disappears along with the component. A component that said it draws its own
|
|
53
|
+
states is never taken away from the person looking at it.
|
|
54
|
+
|
|
55
|
+
`error: true` says the same thing about failure, and it has the same
|
|
56
|
+
consequence: the component stays, so it is still there to offer the way out.
|
|
57
|
+
|
|
58
|
+
A component that delegates nothing needs no `<Loading>` above it. One that
|
|
59
|
+
delegates its wait does, since that is where the suspension lands, and navi says
|
|
60
|
+
so rather than leaving the subtree blank.
|
|
61
|
+
|
|
62
|
+
## `data` and `loading` are independent
|
|
63
|
+
|
|
64
|
+
They answer different questions, so all four combinations happen and each one
|
|
65
|
+
means something different:
|
|
66
|
+
|
|
67
|
+
| `data` | `loading` | what it is | what a screen draws |
|
|
68
|
+
| ----------- | --------- | ----------------------------- | ------------------------ |
|
|
69
|
+
| `undefined` | `true` | first load | a skeleton, animated |
|
|
70
|
+
| set | `true` | a refresh over a known answer | the content, marked busy |
|
|
71
|
+
| set | `false` | settled | the content |
|
|
72
|
+
| `undefined` | `false` | nothing here, nothing coming | an empty state |
|
|
73
|
+
|
|
74
|
+
Two consequences worth stating out loud.
|
|
75
|
+
|
|
76
|
+
**The emptiness test is `data === undefined`, never `loading`.** Reading
|
|
77
|
+
`loading` as "there is nothing to display" blanks the screen on every refresh —
|
|
78
|
+
for a checkbox ticked on one row of a list, the whole list goes.
|
|
79
|
+
|
|
80
|
+
**A skeleton is told whether it is loading; it does not deduce it.** The last
|
|
81
|
+
row of the table is the one that gets forgotten, and it is reachable: a first
|
|
82
|
+
load that failed — its message on screen or dismissed — and a route action whose
|
|
83
|
+
params getter returns false. A skeleton drawn on emptiness alone shimmers there
|
|
84
|
+
forever, claiming a load nobody started.
|
|
85
|
+
|
|
86
|
+
```jsx
|
|
87
|
+
const [items, loading] = useAsyncData(ACTION, { loading: true });
|
|
88
|
+
if (items === undefined) {
|
|
89
|
+
return <ItemsSkeleton loading={loading} />;
|
|
90
|
+
}
|
|
91
|
+
return <ItemList items={items} busy={loading} />;
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
navi's own placeholders take that flag rather than inventing it: `<Text loading>`
|
|
95
|
+
shimmers, `<Text skeleton>` is the same bar held still, and `<Text skeleton
|
|
96
|
+
loading={loading}>` is one that tells the truth either way.
|
|
97
|
+
|
|
98
|
+
**Knowing the count beforehand beats guessing it.** How many skeleton rows to
|
|
99
|
+
draw is a question the response answers too late; a screen that already knows —
|
|
100
|
+
a count carried by the parent resource, a total returned alongside a previous
|
|
101
|
+
page — says so, and `<List loading loadingSkeletonCount={0}>` draws the empty
|
|
102
|
+
state right away instead of three rows that will vanish. Nothing jumps when the
|
|
103
|
+
response lands.
|
|
104
|
+
|
|
105
|
+
## An error is a message on the screen, not the screen
|
|
106
|
+
|
|
107
|
+
`error: true` hands the failure over **beside** the data the action already had,
|
|
108
|
+
not in place of it. A refresh that failed does not unmake the previous answer,
|
|
109
|
+
so a screen that had something to show keeps it and says the failure over it:
|
|
110
|
+
|
|
111
|
+
```jsx
|
|
112
|
+
const [items, loading, error, dismissError] = useAsyncData(ACTION, {
|
|
113
|
+
loading: true,
|
|
114
|
+
error: true,
|
|
115
|
+
});
|
|
116
|
+
if (error && !items) {
|
|
117
|
+
return <ErrorScreen error={error} />; // nothing behind it: the message is the screen
|
|
118
|
+
}
|
|
119
|
+
return (
|
|
120
|
+
<>
|
|
121
|
+
{error && <ErrorStrip error={error} onClose={dismissError} />}
|
|
122
|
+
<ItemList items={items} busy={loading} />
|
|
123
|
+
</>
|
|
124
|
+
);
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`dismissError()` removes the message and nothing else — nothing is asked again,
|
|
128
|
+
because nothing had to be: what closing it reveals was already there. What comes
|
|
129
|
+
back is the same state minus the error, which is why the two branches above stay
|
|
130
|
+
honest afterwards.
|
|
131
|
+
|
|
132
|
+
Taking the whole subtree away is `<ErrorBoundary>`'s job and it is a different
|
|
133
|
+
decision: reach for it when the failure means the screen genuinely cannot be
|
|
134
|
+
drawn, not as a stronger version of the same thing. Which of the two, and where
|
|
135
|
+
an error goes when neither takes it, is
|
|
136
|
+
[error_handling.md](./error_handling.md).
|
|
137
|
+
|
|
138
|
+
## Reference
|
|
139
|
+
|
|
140
|
+
- [error_handling.md](./error_handling.md) — the two kinds of error, where each
|
|
141
|
+
is shown, and what happens to one nobody displays.
|
|
142
|
+
- [list_refresh.md](./list_refresh.md) — the same states for a list, plus what a
|
|
143
|
+
write puts back on screen with no request at all.
|
|
144
|
+
- [actions.md](./actions.md#reading-an-action) — reading an action versus
|
|
145
|
+
running one, `{ run: true }`, `{ onLoad }`.
|
|
146
|
+
- `src/state/demos/use_async_data_demo.html` — one card per shape: delegated,
|
|
147
|
+
loading inline, both inline, owning its request.
|
package/docs/error_handling.md
CHANGED
|
@@ -140,6 +140,15 @@ const [game, loading, error, dismissError] = useAsyncData(gameAction, {
|
|
|
140
140
|
});
|
|
141
141
|
```
|
|
142
142
|
|
|
143
|
+
`game` and `error` arrive **together**: a refresh that failed does not unmake
|
|
144
|
+
the answer the action already had, so the screen keeps what it has and says the
|
|
145
|
+
failure on it, and `dismissError()` takes that message away without asking
|
|
146
|
+
anything again. That is the whole difference between the two shapes — a boundary
|
|
147
|
+
replaces the subtree by definition, this one writes on it — so reach for the
|
|
148
|
+
boundary when the failure means the screen cannot be drawn at all, and for this
|
|
149
|
+
when it means one thing about it is out of date. How the component draws the
|
|
150
|
+
states it is handed is [data_states.md](./data_states.md).
|
|
151
|
+
|
|
143
152
|
And one row rather than the whole list: `<List.Item error>` replaces the row's
|
|
144
153
|
content with the message — what the row stood for did not happen, so drawing it
|
|
145
154
|
as if it had would be a lie.
|
package/docs/list_refresh.md
CHANGED
|
@@ -27,13 +27,15 @@ The short answer:
|
|
|
27
27
|
| never completed yet | `undefined` | `true` |
|
|
28
28
|
| running after success | previous | `true` |
|
|
29
29
|
| completed | fresh | `false` |
|
|
30
|
+
| failed after success | previous | `false` |
|
|
31
|
+
| failed, error closed | `undefined` | `false` |
|
|
30
32
|
|
|
31
33
|
So the emptiness test is `data === undefined`, never `loading`:
|
|
32
34
|
|
|
33
35
|
```js
|
|
34
36
|
const [items, loading] = useAsyncData(ACTION, { loading: true });
|
|
35
37
|
if (items === undefined) {
|
|
36
|
-
return
|
|
38
|
+
return <RadarListSkeleton loading={loading} />; // nothing to show yet
|
|
37
39
|
}
|
|
38
40
|
return <RadarList radars={items} busy={loading} />; // re-read: stay on screen
|
|
39
41
|
```
|
|
@@ -48,10 +50,23 @@ if (loading) {
|
|
|
48
50
|
Read `loading` as "what you are displaying is from before", not "there is
|
|
49
51
|
nothing to display".
|
|
50
52
|
|
|
53
|
+
The two answer different questions — _is there anything to show_ and _is
|
|
54
|
+
anything on its way_ — so a screen reads both, and a skeleton is given `loading`
|
|
55
|
+
rather than deducing it from emptiness. The four combinations, and what each one
|
|
56
|
+
draws, are in [data_states.md](./data_states.md).
|
|
57
|
+
|
|
51
58
|
`<List loading>` is the first-load answer, not the refresh one: it replaces the
|
|
52
59
|
rows with skeletons. Pass it while stale rows exist and they disappear — same
|
|
53
60
|
mistake as `loading ? null :`, one level down.
|
|
54
61
|
|
|
62
|
+
The `failed after success` row is the same rule at the other end: a refresh that
|
|
63
|
+
failed does not unmake the rows either. With `error: true` the failure comes
|
|
64
|
+
back **beside** them, so the list stays and the failure is said over it — a
|
|
65
|
+
strip above the rows, a retry, `dismissError()` to close the strip without
|
|
66
|
+
asking anything again. Taking the rows away is `<ErrorBoundary>`'s job, and only
|
|
67
|
+
when the screen genuinely cannot be drawn — see
|
|
68
|
+
[data_states.md](./data_states.md).
|
|
69
|
+
|
|
55
70
|
## What updates without a request
|
|
56
71
|
|
|
57
72
|
An action's `data` is a computation over the resource store, not a snapshot of
|