@jsenv/navi 0.29.224 → 0.29.225

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.
@@ -114,11 +114,13 @@ to start when unsure which export solves a problem.
114
114
  a boundary. Read before displaying an error by hand or writing a boundary.
115
115
  - `offline.md` — `setNetworkPolicy`: answer from the store, ask nothing, refuse
116
116
  writes politely. Read before caching responses in the app.
117
- - `dynamic_import.md` — a screen's code arrives like its data: `lazy()` makes
118
- the import an action, so `<Loading>`, `<ErrorBoundary>` and `rerun` cover it;
119
- links fetch code on intent, never data; a transition photographs the
120
- fallback; a chunk's `import.meta.css`; why not preact's `lazy`. Read before
121
- an `import()` in a component or a page, and before `prefetch={false}`.
117
+ - `dynamic_import.md` — a screen's code arrives like its data: a page's
118
+ import is a `routeAction`, a component reads its own with
119
+ `useAsyncData(() => import(…))`, so `<Loading>`, `<ErrorBoundary>` and
120
+ `rerun` cover it; links prerun on intent what asks nothing of the address; a
121
+ transition photographs the fallback; a chunk's `import.meta.css`; a chunk
122
+ that does not come is never a bug; why not preact's `lazy`. Read before an
123
+ `import()` in a component or a page, and before `prefetch={false}`.
122
124
 
123
125
  ### Controls and forms
124
126
 
@@ -139,8 +139,8 @@ why they are still concatenated.
139
139
 
140
140
  A module reached by `import()` carries its stylesheet in its chunk, adopted
141
141
  when the chunk runs: a fallback rendered before it cannot use its classes, and
142
- between unlayered sheets the last adopted wins. What follows for a lazily
143
- loaded component is in [dynamic_import.md](./dynamic_import.md).
142
+ between unlayered sheets the last adopted wins. What follows for code loaded
143
+ on demand is in [dynamic_import.md](./dynamic_import.md).
144
144
 
145
145
  ### Browser support: navi's css and your target
146
146
 
@@ -7,14 +7,27 @@ the network is gone or a deploy happened in between — and nothing on screen
7
7
  should have to know whether what it waits for is bytes of data or bytes of
8
8
  code.
9
9
 
10
- ## The import is an action
10
+ So navi has nothing lazy-shaped: **the import is an action**, read by the hook
11
+ a screen already reads its data with. Two shapes, decided by who asks.
11
12
 
12
- `lazy()` turns a loader into a component whose code is an action:
13
+ ## A page: its code is a route action
13
14
 
14
- ```jsx
15
- import { lazy } from "@jsenv/navi";
15
+ The address asks for the page's data through a route action; it asks for the
16
+ page's code the same way, and the two are in flight together from the url
17
+ change:
16
18
 
17
- const GamePage = lazy(() => import("./game_page.jsx"));
19
+ ```jsx
20
+ const GAME_PAGE_ACTION = routeAction(GAME_ROUTE, GAME.GET, () => ({
21
+ gameId: GAME_ROUTE.paramsSignal.value.gameId,
22
+ }));
23
+ const GAME_PAGE_CODE = routeAction(GAME_ROUTE, () =>
24
+ import("./game_page.jsx").then((m) => m.GamePage),
25
+ );
26
+
27
+ const GamePage = () => {
28
+ const [Page] = useAsyncData(GAME_PAGE_CODE);
29
+ return <Page />;
30
+ };
18
31
 
19
32
  <Route>
20
33
  <ErrorBoundary fallback={PageError}>
@@ -25,75 +38,85 @@ const GamePage = lazy(() => import("./game_page.jsx"));
25
38
  </Route>;
26
39
  ```
27
40
 
28
- Everything the data layer already decides then holds for the code, with
29
- nothing more to write:
41
+ Everything the data layer decides then holds for the code, with nothing more
42
+ to write:
30
43
 
31
- - **It starts with the data.** The route action runs on the url change; the
32
- import starts from the render the match triggers, a microtask later — or
33
- earlier, on intent (below). The two are in flight together, and the page
34
- suspends on whichever arrives last.
35
- - **One wait.** The import suspends into the nearest `<Loading>` like a
36
- `useAsyncData` does — the boundary is told it is loading, and draws its
37
- fallback. It counts in the document's busy state like a route action.
44
+ - **One wait.** The page suspends into the nearest `<Loading>` on whichever of
45
+ the two arrives last; the boundary is told it is loading and draws its
46
+ fallback; the routing is busy until both are there.
38
47
  - **One failure.** A rejected import fails the run: the nearest
39
- `<ErrorBoundary>` shows it, `error.action.rerun()` is the way back, and going
40
- somewhere else leaves it behind. A module missing after a deploy is an error
41
- the screen already knows how to show.
42
- - **Fetched once.** A completed run is not run again: coming back to the page
43
- renders synchronously, only the data is asked for per params.
44
-
45
- The loader resolves to the component itself or to the module: its `default`
46
- export, or its only exported function. A module exporting several says which
47
- by returning it from the loader — and two `lazy()` on the same module cost one
48
- fetch, the browser holds a module once:
49
-
50
- ```js
51
- const PlanThumbnail = lazy(() => import("./plan.jsx").then((m) => m.PlanThumbnail));
52
- const PlanDialog = lazy(() => import("./plan.jsx").then((m) => m.PlanDialog));
48
+ `<ErrorBoundary>` shows it, `error.action.rerun()` is the way back, going
49
+ somewhere else leaves it behind.
50
+ - **Fetched once.** A route action left behind is aborted, not reset, and a
51
+ completed one is not run again: coming back to the page renders
52
+ synchronously, only the data is asked for per params.
53
+ - **Fetched ahead.** A route action that asks nothing of the address is prerun
54
+ on intent (below).
55
+
56
+ The page module itself is ordinary — it reads its data with
57
+ `useAsyncData(GAME_PAGE_ACTION)` like any page — and imports the routes module
58
+ back for it; a dynamic import is not a load-order cycle.
59
+
60
+ ## A component inside a page that stays: it reads its own code
61
+
62
+ A plan drawn on demand inside a page that stays knows where it is drawn and
63
+ what stands there — a frame, a line, a button. `useAsyncData` takes a
64
+ function: the request this component owns, made into an action once on the
65
+ first render and kept for the life of the instance, its answer as the data.
66
+ The wait and the failure are then drawn where the frame is, and no boundary is
67
+ involved:
68
+
69
+ ```jsx
70
+ const PlanSection = () => {
71
+ const [Plan, loading, error] = useAsyncData(
72
+ () => import("./plan.jsx").then((m) => m.Plan),
73
+ { loading: true, error: true },
74
+ );
75
+ return (
76
+ <PlanFrame>
77
+ {loading ? <Text>…</Text> : null}
78
+ {error ? (
79
+ <Button action={() => error.action.rerun()}>Réessayer</Button>
80
+ ) : null}
81
+ {Plan ? <Plan /> : null}
82
+ </PlanFrame>
83
+ );
84
+ };
53
85
  ```
54
86
 
55
- ## Where the boundary goes
87
+ Delegating instead (`useAsyncData(fn)` alone, under a `<Loading>` around the
88
+ frame) is the same hook with the same rule as for data: **the `<Loading>` goes
89
+ where the fallback should land.** A boundary around the whole router takes the
90
+ router away while a page loads — top bar, tabs and all.
56
91
 
57
- The rule of the data layer applies unchanged: **the `<Loading>` goes where the
58
- fallback should land**, and a `lazy()` element needs one above it exactly as a
59
- page reading its data does. A boundary around the whole router takes the
60
- router away while a page loads — top bar, tabs and all — and puts the fallback
61
- in its place. Written between the container and its branches, it holds the
62
- page's box and nothing else (see [navigation.md](./navigation.md), "Loading
63
- data"). A component fetched on demand inside a page that stays gets its own
64
- `<Loading>` around it, with the frame of what it stands in for as the
65
- fallback.
92
+ This shape starts at the render and is per instance: a component fetched this
93
+ way in three places asks three times (the browser fetches the module once),
94
+ and coming back to it suspends for the microtask the import takes to answer
95
+ from the module map. A page wants the route action shape; a section inside a
96
+ page rarely notices.
66
97
 
67
98
  Nothing has to be kept out of a subtree loaded this way: the suspension
68
99
  happens before the module's components exist, so nothing inside it is parked
69
- by the code arriving. A subtree parked later is parked by a data suspension,
70
- which is the same story with or without a split.
100
+ by the code arriving.
71
101
 
72
102
  ## Ahead of the render: intent
73
103
 
74
- **A link fetches the code of where it leads when the pointer or the focus
75
- reaches it.** `<Link>` and `<Button>` with an `href` or a `route` do it by
76
- default; `prefetch={false}` opts one out — a destination not worth fetching on
77
- a hover. The url is matched against the routes on screen, a section and the
78
- page inside it alike, and each fetches what its router registered for it.
79
-
80
- Two things are deliberately NOT part of it:
104
+ **A link preloads where it leads when the pointer or the focus reaches it.**
105
+ `<Link>` and `<Button>` with an `href` or a `route` do it by default;
106
+ `prefetch={false}` opts one out. The url is matched against the routes on
107
+ screen, a section and the page inside it alike, and what is prerun is **every
108
+ route action that asks nothing of the address** — a page's code, a read
109
+ without params. An action whose params come from the address is never
110
+ prefetched: a prefetch has no address, the id may still be chosen.
111
+ `routeAction(route, action, undefined, { prefetch: false })` keeps a
112
+ param-less read that is not worth a hover out of it.
81
113
 
82
- - **Only code is fetched, never data.** The data is the route action's, asked
83
- for on arrival with the params the address holds. A prefetch has no such
84
- address yet — the id may still be chosen — and a read ahead of need is a
85
- decision the app takes itself, with `prerun()` on the binding it wants warm.
86
- - **A prefetch that fails is forgotten**, not reported: nothing on screen asked
87
- for it. The visit that needs the code asks again and shows its own failure.
114
+ A prefetch that fails is forgotten, not reported: nothing on screen asked for
115
+ it. The arrival asks again and shows its own failure.
88
116
 
89
- The same fetch, from code: `route.preload()` for a route something is about to
117
+ The same from code: `route.preload()` for a route something is about to
90
118
  navigate to, `preloadUrl(url)` for an address. A press that computes its
91
- destination has nothing to preload — the navigation itself fetches the code.
92
-
93
- Which routers know: a route's lazy elements register when the router holding
94
- them renders. A section whose sub-router is inside its own chunk registers its
95
- pages once the section is on screen; before that, intent on a link to a
96
- sub-page fetches the section.
119
+ destination has nothing to preload — the navigation itself asks.
97
120
 
98
121
  ## What a transition photographs
99
122
 
@@ -124,13 +147,31 @@ consequences:
124
147
  in the build fixes. A rule that depends on it is a rule to make more
125
148
  specific.
126
149
 
127
- ## Preact's `lazy()` is not this one
150
+ ## When the code does not come
151
+
152
+ Someone has the app open, a deploy moves the chunks, and the next `import()`
153
+ is a 404; or the network is gone. The browser says both the same way — a
154
+ `TypeError` with no status — and neither is a bug of the page: there is
155
+ nothing in the app's code to point at.
156
+
157
+ The component reading its own code says it where it stands, as above. For a
158
+ page, the failure reaches the boundary like a failed read does; a boundary
159
+ that shows only what came from the network and rethrows the rest as a bug
160
+ recognises an import by its action — `error.action` is the route action the
161
+ app declared for the code — rather than by the browser's message. Either way
162
+ `error.action.rerun()` retries; a retry failing the same way says the document
163
+ is stale, and **reloading it** is what brings the new code. Navi does not do
164
+ it on its own — a document reloading itself is a loop waiting to happen — the
165
+ screen says it.
166
+
167
+ ## Preact's `lazy()` is not this
128
168
 
129
- `preact/compat` has a `lazy()` too. It suspends without saying anything to
169
+ `preact/compat` has a `lazy()`. It suspends without saying anything to
130
170
  `<Loading>`: the boundary keeps the last reason it knows — "idle" by default —
131
171
  and draws nothing while the module loads; a failure is not an action's, so no
132
172
  `rerun` brings the page back. Navi warns in dev when a `<Loading>` catches a
133
- suspension it cannot attribute to an action. Import `lazy` from navi.
173
+ suspension it cannot attribute to an action. Read the import through an
174
+ action.
134
175
 
135
176
  ## The build
136
177
 
@@ -140,7 +181,9 @@ configure.
140
181
 
141
182
  ## Reference
142
183
 
143
- - `src/nav/demos/code_splitting/code_splitting_demo.html` — two pages fetched
144
- on demand next to a route action, one prefetched on hover and one opted out,
145
- with the import drawn on the backend's frontier so both waits can be watched.
146
- - `src/state/async/lazy.jsx`, `src/nav/use_preload_on_intent.js`
184
+ - `src/nav/demos/code_splitting/code_splitting_demo.html` — two pages whose
185
+ code is a route action, one prefetched on hover and one opted out, and a
186
+ component inside a page that stays, drawing its own wait and failure; every
187
+ import is drawn on the backend's frontier so a wait or a failure can be
188
+ played by hand.
189
+ - `src/nav/route_action.js`, `src/nav/use_preload_on_intent.js`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/navi",
3
- "version": "0.29.224",
3
+ "version": "0.29.225",
4
4
  "type": "module",
5
5
  "description": "Library of components including navigation to create frontend applications",
6
6
  "repository": {