@rangojs/router 0.24.3 → 0.25.0
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 +1 -0
- package/dist/bin/rango.js +363 -39
- package/dist/testing/vitest.js +37 -4
- package/dist/types/browser/event-controller.d.ts +24 -5
- package/dist/types/browser/intercept-utils.d.ts +5 -2
- package/dist/types/browser/merge-segment-loaders.d.ts +0 -23
- package/dist/types/browser/navigation-transaction.d.ts +5 -5
- package/dist/types/browser/partial-update.d.ts +21 -13
- package/dist/types/browser/prefetch/cache.d.ts +8 -0
- package/dist/types/browser/rango-marker.d.ts +7 -0
- package/dist/types/browser/react/filter-segment-order.d.ts +10 -0
- package/dist/types/browser/react/use-handle.d.ts +20 -0
- package/dist/types/browser/react/use-href.d.ts +6 -0
- package/dist/types/browser/react/use-router.d.ts +6 -1
- package/dist/types/browser/response-adapter.d.ts +5 -3
- package/dist/types/browser/scroll-restoration.d.ts +9 -3
- package/dist/types/browser/segment-reconciler.d.ts +10 -13
- package/dist/types/browser/types.d.ts +38 -8
- package/dist/types/build/route-types/per-module-writer.d.ts +12 -0
- package/dist/types/cache/cf/cf-shell-frame.d.ts +2 -0
- package/dist/types/cache/types.d.ts +8 -0
- package/dist/types/client-urls/client-root.d.ts +4 -1
- package/dist/types/client-urls/client-urls.d.ts +28 -2
- package/dist/types/client-urls/navigation.d.ts +6 -8
- package/dist/types/client-urls/revalidate-registry.d.ts +1 -1
- package/dist/types/client-urls/server-projection.d.ts +20 -5
- package/dist/types/client-urls/types.d.ts +35 -17
- package/dist/types/client.d.ts +2 -2
- package/dist/types/client.rsc.d.ts +2 -2
- package/dist/types/decode-loader-results.d.ts +28 -7
- package/dist/types/href-client.d.ts +3 -1
- package/dist/types/index.rsc.d.ts +1 -1
- package/dist/types/loader-redirect.d.ts +2 -3
- package/dist/types/outlet-context.d.ts +7 -6
- package/dist/types/prefetch-markers.d.ts +14 -5
- package/dist/types/route-content-wrapper.d.ts +17 -14
- package/dist/types/route-definition/helpers-types.d.ts +7 -2
- package/dist/types/route-definition/resolve-handler-use.d.ts +6 -1
- package/dist/types/route-definition/rewrite.d.ts +48 -5
- package/dist/types/route-types.d.ts +2 -2
- package/dist/types/router/handler-context.d.ts +14 -2
- package/dist/types/router/intercept-resolution.d.ts +19 -46
- package/dist/types/router/lazy-includes.d.ts +7 -0
- package/dist/types/router/match-middleware/intercept-resolution.d.ts +2 -2
- package/dist/types/router/middleware.d.ts +0 -16
- package/dist/types/router/navigation-snapshot.d.ts +2 -0
- package/dist/types/router/prefetch-cache-ttl.d.ts +8 -11
- package/dist/types/router/revalidation.d.ts +8 -1
- package/dist/types/router/router-context.d.ts +4 -16
- package/dist/types/router/router-interfaces.d.ts +0 -6
- package/dist/types/router/router-options.d.ts +4 -7
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +31 -8
- package/dist/types/router/segment-resolution/revalidation.d.ts +38 -4
- package/dist/types/router/segment-resolution.d.ts +1 -1
- package/dist/types/router/segment-wrappers.d.ts +3 -5
- package/dist/types/router/substitute-pattern-params.d.ts +19 -2
- package/dist/types/router/tracing.d.ts +2 -2
- package/dist/types/rsc/full-payload.d.ts +14 -2
- package/dist/types/rsc/helpers.d.ts +8 -0
- package/dist/types/rsc/manifest-init.d.ts +20 -0
- package/dist/types/rsc/prefetch-render-error.d.ts +22 -0
- package/dist/types/rsc/progressive-enhancement.d.ts +20 -0
- package/dist/types/rsc/rewrite-passes.d.ts +12 -2
- package/dist/types/rsc/server-action.d.ts +6 -0
- package/dist/types/rsc/shell-capture-constants.d.ts +10 -1
- package/dist/types/rsc/shell-capture.d.ts +7 -0
- package/dist/types/rsc/types.d.ts +5 -0
- package/dist/types/segment-system.d.ts +15 -0
- package/dist/types/server/context.d.ts +64 -4
- package/dist/types/server/request-context.d.ts +41 -4
- package/dist/types/server/request-headers.d.ts +12 -0
- package/dist/types/server/rewrite-trail.d.ts +8 -1
- package/dist/types/testing/e2e/index.d.ts +1 -1
- package/dist/types/testing/flight.entry.d.ts +1 -1
- package/dist/types/testing/index.d.ts +1 -1
- package/dist/types/testing/internal/client-urls-projection.d.ts +4 -3
- package/dist/types/testing/render-route.d.ts +21 -11
- package/dist/types/testing/run-loader.d.ts +7 -0
- package/dist/types/testing/run-middleware.d.ts +7 -0
- package/dist/types/testing/run-revalidate.d.ts +5 -4
- package/dist/types/testing/serve-shell-request.d.ts +41 -4
- package/dist/types/testing/shell-status.d.ts +17 -0
- package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
- package/dist/types/testing/vitest.d.ts +10 -1
- package/dist/types/thenable-status.d.ts +0 -6
- package/dist/types/types/handler-context.d.ts +12 -1
- package/dist/types/types/loader-types.d.ts +4 -3
- package/dist/types/types/route-entry.d.ts +2 -1
- package/dist/types/urls/path-helper-types.d.ts +4 -2
- package/dist/types/urls/path-helper.d.ts +5 -0
- package/dist/types/vite/discovery/client-urls-projection.d.ts +55 -5
- package/dist/types/vite/discovery/state.d.ts +20 -0
- package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +28 -16
- package/dist/types/vite/plugins/hoist/source.d.ts +8 -7
- package/dist/types/vite/plugins/virtual-entries.d.ts +6 -0
- package/dist/types/vite/utils/prerender-utils.d.ts +7 -0
- package/dist/vite/index.js +6816 -6547
- package/package.json +1 -1
- package/skills/app-structure/SKILL.md +255 -0
- package/skills/app-structure/content-site.md +333 -0
- package/skills/app-structure/e-commerce.md +435 -0
- package/skills/app-structure/large-app.md +885 -0
- package/skills/app-structure/multi-app.md +280 -0
- package/skills/breadcrumbs/SKILL.md +6 -1
- package/skills/caching/SKILL.md +3 -1
- package/skills/catalog.json +12 -0
- package/skills/client-urls/SKILL.md +195 -32
- package/skills/composability/SKILL.md +10 -8
- package/skills/handler-use/SKILL.md +48 -9
- package/skills/hooks/handle-and-actions.md +104 -0
- package/skills/hooks/navigation.md +24 -10
- package/skills/hooks/urls.md +1 -1
- package/skills/i18n/SKILL.md +2 -1
- package/skills/intercept/SKILL.md +60 -29
- package/skills/layout/SKILL.md +6 -3
- package/skills/links/SKILL.md +151 -17
- package/skills/loader/SKILL.md +32 -10
- package/skills/middleware/SKILL.md +111 -15
- package/skills/observability/SKILL.md +11 -0
- package/skills/parallel/SKILL.md +5 -5
- package/skills/ppr/SKILL.md +43 -8
- package/skills/prerender/SKILL.md +1 -2
- package/skills/rango/SKILL.md +3 -0
- package/skills/response-routes/SKILL.md +4 -1
- package/skills/scripts/SKILL.md +12 -1
- package/skills/server-actions/SKILL.md +60 -20
- package/skills/testing/SKILL.md +1 -0
- package/skills/testing/client-components.md +13 -10
- package/skills/testing/loader.md +25 -24
- package/skills/testing/middleware.md +11 -11
- package/skills/testing/response-routes.md +4 -1
- package/skills/testing/serve-shell-request.md +85 -21
- package/skills/ux-architecture/SKILL.md +980 -0
- package/skills/view-transitions/SKILL.md +43 -10
- package/src/__augment-tests__/augmented.check.ts +28 -0
- package/src/browser/event-controller.ts +44 -18
- package/src/browser/intercept-utils.ts +3 -2
- package/src/browser/merge-segment-loaders.ts +0 -85
- package/src/browser/navigation-bridge.ts +119 -98
- package/src/browser/navigation-client.ts +20 -15
- package/src/browser/navigation-store.ts +4 -2
- package/src/browser/navigation-transaction.ts +9 -8
- package/src/browser/partial-update.ts +90 -32
- package/src/browser/prefetch/cache.ts +8 -0
- package/src/browser/prefetch/fetch.ts +13 -3
- package/src/browser/rango-marker.ts +13 -0
- package/src/browser/react/NavigationProvider.tsx +25 -7
- package/src/browser/react/filter-segment-order.ts +13 -0
- package/src/browser/react/use-action.ts +9 -15
- package/src/browser/react/use-handle.ts +63 -71
- package/src/browser/react/use-href.tsx +24 -5
- package/src/browser/react/use-link-status.ts +1 -1
- package/src/browser/react/use-navigation.ts +30 -40
- package/src/browser/react/use-reverse.ts +4 -17
- package/src/browser/react/use-router.ts +37 -6
- package/src/browser/react/use-segments.ts +8 -12
- package/src/browser/response-adapter.ts +11 -5
- package/src/browser/rsc-router.tsx +3 -1
- package/src/browser/scroll-restoration.ts +34 -20
- package/src/browser/segment-reconciler.ts +11 -29
- package/src/browser/server-action-bridge.ts +52 -16
- package/src/browser/types.ts +40 -8
- package/src/build/generate-manifest.ts +12 -1
- package/src/build/route-types/codegen.ts +6 -2
- package/src/build/route-types/per-module-writer.ts +94 -61
- package/src/cache/cache-runtime.ts +9 -0
- package/src/cache/cache-scope.ts +2 -1
- package/src/cache/cf/cf-cache-store.ts +2 -0
- package/src/cache/cf/cf-shell-frame.ts +3 -0
- package/src/cache/types.ts +8 -0
- package/src/cache/vercel/vercel-cache-store.ts +6 -1
- package/src/client-urls/client-root.tsx +10 -5
- package/src/client-urls/client-urls.ts +246 -166
- package/src/client-urls/navigation.ts +45 -38
- package/src/client-urls/revalidate-registry.ts +12 -3
- package/src/client-urls/server-projection.ts +286 -75
- package/src/client-urls/types.ts +40 -16
- package/src/client.rsc.tsx +11 -1
- package/src/client.tsx +9 -1
- package/src/decode-loader-results.ts +64 -31
- package/src/href-client.ts +3 -1
- package/src/index.rsc.ts +1 -0
- package/src/index.ts +12 -4
- package/src/loader-redirect.tsx +2 -3
- package/src/outlet-context.ts +7 -6
- package/src/prefetch-markers.ts +15 -5
- package/src/reverse.ts +3 -3
- package/src/route-content-wrapper.tsx +19 -17
- package/src/route-definition/dsl-helpers.ts +27 -7
- package/src/route-definition/helpers-types.ts +7 -2
- package/src/route-definition/resolve-handler-use.ts +23 -10
- package/src/route-definition/rewrite.ts +86 -19
- package/src/route-types.ts +1 -2
- package/src/router/find-match.ts +37 -1
- package/src/router/handler-context.ts +85 -40
- package/src/router/intercept-resolution.ts +161 -307
- package/src/router/lazy-includes.ts +43 -27
- package/src/router/loader-resolution.ts +21 -1
- package/src/router/manifest.ts +6 -8
- package/src/router/match-api.ts +11 -1
- package/src/router/match-middleware/background-revalidation.ts +2 -4
- package/src/router/match-middleware/cache-lookup.ts +16 -20
- package/src/router/match-middleware/intercept-resolution.ts +41 -60
- package/src/router/match-result.ts +31 -2
- package/src/router/middleware.ts +6 -147
- package/src/router/navigation-snapshot.ts +38 -2
- package/src/router/prefetch-cache-ttl.ts +10 -18
- package/src/router/prerender-match.ts +2 -1
- package/src/router/revalidation.ts +24 -3
- package/src/router/router-context.ts +5 -18
- package/src/router/router-interfaces.ts +0 -7
- package/src/router/router-options.ts +4 -7
- package/src/router/segment-resolution/fresh.ts +6 -2
- package/src/router/segment-resolution/helpers.ts +7 -1
- package/src/router/segment-resolution/prefetch-deferral.ts +62 -12
- package/src/router/segment-resolution/revalidation.ts +75 -35
- package/src/router/segment-resolution.ts +0 -1
- package/src/router/segment-wrappers.ts +8 -13
- package/src/router/substitute-pattern-params.ts +37 -2
- package/src/router/tracing.ts +2 -2
- package/src/router.ts +2 -9
- package/src/rsc/full-payload.ts +26 -4
- package/src/rsc/handler.ts +83 -41
- package/src/rsc/helpers.ts +13 -0
- package/src/rsc/manifest-init.ts +56 -3
- package/src/rsc/prefetch-render-error.ts +38 -0
- package/src/rsc/progressive-enhancement.ts +161 -211
- package/src/rsc/rewrite-passes.ts +56 -9
- package/src/rsc/rsc-rendering.ts +79 -23
- package/src/rsc/server-action.ts +25 -2
- package/src/rsc/shell-capture-constants.ts +22 -0
- package/src/rsc/shell-capture.ts +11 -0
- package/src/rsc/types.ts +5 -0
- package/src/segment-system.tsx +125 -119
- package/src/server/context.ts +96 -5
- package/src/server/handle-store.ts +3 -2
- package/src/server/request-context.ts +42 -3
- package/src/server/request-headers.ts +32 -0
- package/src/server/rewrite-trail.ts +55 -9
- package/src/ssr/ssr-root.tsx +6 -10
- package/src/testing/dispatch.ts +4 -5
- package/src/testing/e2e/index.ts +1 -0
- package/src/testing/flight.entry.ts +1 -0
- package/src/testing/index.ts +1 -0
- package/src/testing/internal/client-urls-projection.ts +31 -11
- package/src/testing/render-route.tsx +83 -40
- package/src/testing/run-loader.ts +26 -2
- package/src/testing/run-middleware.ts +10 -0
- package/src/testing/run-revalidate.ts +13 -6
- package/src/testing/serve-shell-request.ts +77 -4
- package/src/testing/shell-status.ts +40 -0
- package/src/testing/vitest-stubs/version.ts +1 -0
- package/src/testing/vitest.ts +74 -7
- package/src/thenable-status.ts +0 -11
- package/src/types/handler-context.ts +12 -1
- package/src/types/loader-types.ts +4 -3
- package/src/types/route-entry.ts +2 -1
- package/src/urls/include-helper.ts +38 -11
- package/src/urls/path-helper-types.ts +4 -2
- package/src/urls/path-helper.ts +16 -3
- package/src/vite/discovery/client-urls-projection.ts +205 -16
- package/src/vite/discovery/discover-routers.ts +2 -2
- package/src/vite/discovery/prerender-collection.ts +2 -6
- package/src/vite/discovery/state.ts +26 -0
- package/src/vite/plugins/client-urls-revalidate-hoist.ts +190 -77
- package/src/vite/plugins/expose-internal-ids.ts +46 -40
- package/src/vite/plugins/hoist/source.ts +8 -7
- package/src/vite/plugins/transition-when-hoist.ts +1 -0
- package/src/vite/plugins/version-plugin.ts +7 -0
- package/src/vite/plugins/version.d.ts +7 -0
- package/src/vite/plugins/virtual-entries.ts +20 -0
- package/src/vite/rango.ts +4 -1
- package/src/vite/router-discovery.ts +39 -7
- package/src/vite/utils/prerender-utils.ts +17 -0
|
@@ -142,6 +142,85 @@ At the top level of the builder only `path()`, `layout()` and `intercept()`
|
|
|
142
142
|
are accepted, and the builder must define at least one `path()`. Any helper
|
|
143
143
|
name not in this table throws (`clientUrls() has no route() helper`).
|
|
144
144
|
|
|
145
|
+
`loader`, `loading`, `revalidate` and `transition` are also exported from
|
|
146
|
+
`@rangojs/router/client`, the same functions the builder hands out, for a
|
|
147
|
+
component's own items (next section).
|
|
148
|
+
|
|
149
|
+
## A component's own items: `Component.use`
|
|
150
|
+
|
|
151
|
+
A page, layout or intercept component can carry its own items, as a server
|
|
152
|
+
handler does with `.use` (`/handler-use`): its loaders, its skeleton and its
|
|
153
|
+
revalidation rules travel with it instead of being repeated at every `path()`
|
|
154
|
+
that mounts it. Build them with the helpers from `@rangojs/router/client`. The
|
|
155
|
+
component's module needs no `clientUrls()`, and no `"use client"` of its own
|
|
156
|
+
when a group imports it.
|
|
157
|
+
|
|
158
|
+
```tsx
|
|
159
|
+
// src/dash/Dashboard.tsx
|
|
160
|
+
"use client";
|
|
161
|
+
import { loader, loading, revalidate, useLoader } from "@rangojs/router/client";
|
|
162
|
+
import * as StatsActions from "./stats.actions";
|
|
163
|
+
import { StatsLoader } from "./stats.loader";
|
|
164
|
+
|
|
165
|
+
export function Dashboard() {
|
|
166
|
+
const { data } = useLoader(StatsLoader);
|
|
167
|
+
return <StatsView stats={data} />;
|
|
168
|
+
}
|
|
169
|
+
Dashboard.use = () => [
|
|
170
|
+
loader(StatsLoader, () => [
|
|
171
|
+
revalidate(({ isAction }) => isAction(StatsActions)),
|
|
172
|
+
]),
|
|
173
|
+
loading(<DashboardSkeleton />),
|
|
174
|
+
];
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
```tsx
|
|
178
|
+
// src/urls/app.client.tsx
|
|
179
|
+
"use client";
|
|
180
|
+
import { clientUrls } from "@rangojs/router/client";
|
|
181
|
+
import { Dashboard } from "../dash/Dashboard";
|
|
182
|
+
|
|
183
|
+
export default clientUrls(({ path }) => [
|
|
184
|
+
path("/dash", Dashboard, { name: "dash" }),
|
|
185
|
+
]);
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
| Component mounted by | Its `.use` may return |
|
|
189
|
+
| -------------------- | ---------------------------------------------- |
|
|
190
|
+
| `path()` | `loader()`, `loading()`, `transition()` |
|
|
191
|
+
| `layout()` | `loader()`, `loading()` |
|
|
192
|
+
| `intercept()` | `loader()` (without `ssr: false`), `loading()` |
|
|
193
|
+
|
|
194
|
+
- **Merge order is the server's.** The component's items come first and the
|
|
195
|
+
mount site's own items second: an explicit `loading()` or `transition()`
|
|
196
|
+
wins, loaders accumulate (the same definition in both gives two loaders, as
|
|
197
|
+
`loader(Def)` twice does). A layout component's loaders are layout loaders,
|
|
198
|
+
shared by every route below it.
|
|
199
|
+
- **No routes inside `.use`.** `path()`, `layout()` and `intercept()` there
|
|
200
|
+
throw (`clientUrls() does not support path() inside Dashboard.use`): route
|
|
201
|
+
types and the generated `.gen.ts` are read from the builder's own items.
|
|
202
|
+
Unlike a server layout handler's `.use`, a client layout's cannot add
|
|
203
|
+
routes.
|
|
204
|
+
- **`.use` runs when the group is defined**, once per mount site (in the
|
|
205
|
+
browser, during SSR and in discovery), never at render. The error for an
|
|
206
|
+
item the site does not take names the component.
|
|
207
|
+
- **A predicate in `.use` runs on the server,** as one in the builder does:
|
|
208
|
+
the build hoists it out of the component's module with the rules below,
|
|
209
|
+
and it registers when a group whose routes use the component loads on the
|
|
210
|
+
server, wherever the module sits: behind barrels or in another workspace
|
|
211
|
+
package. A predicate in `node_modules` is not hoisted,
|
|
212
|
+
and route discovery fails naming its loader.
|
|
213
|
+
- **Import the helpers from `@rangojs/router/client`.** The same names from
|
|
214
|
+
`@rangojs/router` are the server DSL's; called in a client module they
|
|
215
|
+
throw, pointing to `@rangojs/router/client`.
|
|
216
|
+
- In dev, an edit of a component's `.use` refreshes its groups' projections
|
|
217
|
+
and predicates, as an edit of the group does.
|
|
218
|
+
- Under `isolatedDeclarations` (a library's sources), an exported function
|
|
219
|
+
cannot take an assigned property: declare the component unexported,
|
|
220
|
+
assign `.use`, and export it through a const typed `ClientUrlComponent`.
|
|
221
|
+
- A `createLoader()` definition's own `.use` (`Def.use`) is not read inside
|
|
222
|
+
`clientUrls()` yet: put its `revalidate()` on the `loader()` call.
|
|
223
|
+
|
|
145
224
|
`include`, `parallel`, `cache`, `middleware`, `errorBoundary`,
|
|
146
225
|
`notFoundBoundary` **throw** — and that is a design position, not a gap.
|
|
147
226
|
Client route groups are a PERFORMANCE surface (instant presentation, held
|
|
@@ -201,10 +280,11 @@ the arguments and type of a `urls()` loader's (`ShouldRevalidateFn` from
|
|
|
201
280
|
browser before the request goes out; there is no client-computed decision and
|
|
202
281
|
no hybrid.
|
|
203
282
|
|
|
204
|
-
You still write the predicate in the `"use client"` module
|
|
205
|
-
plugin hoists each `revalidate()` argument
|
|
206
|
-
key in the browser and SSR copies, and
|
|
207
|
-
group's module loads on the server
|
|
283
|
+
You still write the predicate in the `"use client"` module, or in a
|
|
284
|
+
component's `.use`. The rango Vite plugin hoists each `revalidate()` argument
|
|
285
|
+
into a server-only module, leaves a key in the browser and SSR copies, and
|
|
286
|
+
registers the predicate when the group's module loads on the server, with
|
|
287
|
+
those of the modules it imports (a lazy `include()` stays lazy). The cost
|
|
208
288
|
is a rule: **the argument may reference imports and globals only.** Anything
|
|
209
289
|
else is a build error naming `file:line:column`, starting
|
|
210
290
|
`[rango] clientUrls() revalidate() at ...: revalidate() runs on the server; ...`.
|
|
@@ -213,6 +293,14 @@ else is a build error naming `file:line:column`, starting
|
|
|
213
293
|
(`revalidate(isCartAction)`), a namespace member (`P.cart`), a call of an
|
|
214
294
|
imported factory (`makePredicate("cart")`), and globals such as `URL`,
|
|
215
295
|
`Boolean` and `import.meta.env`.
|
|
296
|
+
- `revalidate(true)` / `revalidate(false)`, shorthand for `() => true` /
|
|
297
|
+
`() => false`: a boolean LITERAL stays in place (nothing to hoist). Any
|
|
298
|
+
other argument is hoisted whatever it evaluates to, because the build cannot
|
|
299
|
+
tell an imported boolean from an imported predicate: the server evaluates
|
|
300
|
+
it, and a boolean result is the same constant answer
|
|
301
|
+
(`revalidate(HOLD_SESSION)` with an imported flag works). A value that is
|
|
302
|
+
neither fails when the server mounts the group, naming the route and
|
|
303
|
+
loader.
|
|
216
304
|
- A binding declared in the same `"use client"` module is rejected: move the
|
|
217
305
|
predicate to its own module without `"use client"` and import it, or write it
|
|
218
306
|
inline. A binding of an enclosing function is rejected: read it from the
|
|
@@ -223,7 +311,8 @@ else is a build error naming `file:line:column`, starting
|
|
|
223
311
|
request (a cookie or a search param) and read it from the argument, or
|
|
224
312
|
refresh with `useRefreshLoaders`.
|
|
225
313
|
- An `async` or generator function, a spread, a second argument, no argument,
|
|
226
|
-
or a
|
|
314
|
+
or a literal that is neither a function nor a boolean (object, array,
|
|
315
|
+
string, number or `null`) is rejected.
|
|
227
316
|
- An import of `client-only`, or of a module whose directive prologue is
|
|
228
317
|
`"use client"`, is rejected (on the server its exports are client
|
|
229
318
|
references). A `"use server"` import is fine: on the server it is the real
|
|
@@ -273,10 +362,10 @@ with the ternary above instead (same rule as a server-tree loader). A bare
|
|
|
273
362
|
`isAction(refs)` is a hard `false` on navigation too, so the loader stops
|
|
274
363
|
refetching when params change.
|
|
275
364
|
|
|
276
|
-
`intercept()` loaders inside `clientUrls()`
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
365
|
+
`intercept()` loaders inside `clientUrls()` take `revalidate()` the same way:
|
|
366
|
+
the build moves the predicate to the server, and it runs there with the same
|
|
367
|
+
arguments, `actionResult` included, so an action from the modal re-runs only
|
|
368
|
+
the loaders whose predicate says so.
|
|
280
369
|
|
|
281
370
|
A group `layout()` loader revalidates as a server layout's loader does. With no
|
|
282
371
|
`revalidate()` a navigation keeps it, whichever route of the layout it goes to
|
|
@@ -292,7 +381,7 @@ currentParams.slug !== nextParams.slug ? true : defaultShouldRevalidate)`.
|
|
|
292
381
|
|
|
293
382
|
Two scars worth copying:
|
|
294
383
|
|
|
295
|
-
- A blunt `()
|
|
384
|
+
- A blunt `revalidate(false)` keeps serving the OLD product on product→product
|
|
296
385
|
navigations (same route, new param). Make predicates param-sensitive:
|
|
297
386
|
return `defaultShouldRevalidate` when the identifying param changed.
|
|
298
387
|
- One action, per-loader outcomes: a cart badge loader revalidates on cart
|
|
@@ -312,8 +401,8 @@ Everything after that is decided on the server, while it handles that request:
|
|
|
312
401
|
- **The browser's caches.** When the response arrives the browser marks its
|
|
313
402
|
history cache stale and drops its prefetches, unless the action called
|
|
314
403
|
`keepClientCache()` (`/server-actions` → "Client Cache After an Action").
|
|
315
|
-
- **A loader that fails on its rerun** reaches your `onError` on the server
|
|
316
|
-
the
|
|
404
|
+
- **A loader that fails on its rerun** reaches your `onError` on the server,
|
|
405
|
+
and in the browser it throws at its read, below.
|
|
317
406
|
|
|
318
407
|
A form's React state survives an action's revalidation in a group, as on a
|
|
319
408
|
`urls()` route: the commit renders into the mounted page instead of remounting
|
|
@@ -322,17 +411,79 @@ it, so a controlled input keeps what the user typed.
|
|
|
322
411
|
### A loader that fails on its rerun
|
|
323
412
|
|
|
324
413
|
The action has already run when its loaders rerun, so a loader that throws
|
|
325
|
-
there does not undo it.
|
|
414
|
+
there does not undo it. It behaves as on a navigation, on every route (a group
|
|
415
|
+
route or a `urls()` route, with or without `loading()`), and whether the
|
|
416
|
+
action was called directly or from `<form action>`, `useActionState` or
|
|
417
|
+
`useTransition`:
|
|
418
|
+
|
|
419
|
+
- **The call resolves** with the action's return value.
|
|
420
|
+
- **The loader throws in place,** at its `useLoader` read. The nearest
|
|
421
|
+
boundary to the read catches it: your own React error boundary around the
|
|
422
|
+
read, else the `errorBoundary()` around the group's `include()` mount (or
|
|
423
|
+
the route's, in `urls()`), else the router's root error boundary.
|
|
424
|
+
- **Everything outside that boundary stays,** with its React state: a form
|
|
425
|
+
next to the failed list keeps what the user typed. Loaders that reran
|
|
426
|
+
without error commit their new data in the same commit.
|
|
427
|
+
- **The error is reported**: to your `onError` on the server (phase
|
|
428
|
+
`"loader"`), and in the browser as `revalidationError` on the action's
|
|
429
|
+
`useAction()` state. `error` stays `null` there and `result` holds the
|
|
430
|
+
action's return value, so "saved, but the list could not refresh" reads
|
|
431
|
+
apart from "the save failed".
|
|
432
|
+
- **A later rerun that succeeds clears it.** An `errorBoundary()` fallback
|
|
433
|
+
clears on the next action or navigation that reruns the loader without
|
|
434
|
+
error. Your own boundary resets by its own rule: key the reset on data that
|
|
435
|
+
reruns with the loader and is read outside the boundary, so the reset lands
|
|
436
|
+
in the same render as the new data. A reset when the action settles can run
|
|
437
|
+
before React rendered the commit and catch the old failure again.
|
|
438
|
+
|
|
439
|
+
```tsx
|
|
440
|
+
// in the group's "use client" module
|
|
441
|
+
function StockPanel() {
|
|
442
|
+
// Reruns with every save and never throws.
|
|
443
|
+
const { data: status } = useLoader(SaveStatusLoader);
|
|
444
|
+
return (
|
|
445
|
+
<StockBoundary resetKey={status.version}>
|
|
446
|
+
<StockList /> {/* useLoader(StockLoader) throws here on a failed rerun */}
|
|
447
|
+
</StockBoundary>
|
|
448
|
+
);
|
|
449
|
+
}
|
|
326
450
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
451
|
+
function SaveStock({ sku }: { sku: string }) {
|
|
452
|
+
const { revalidationError } = useAction(saveStock);
|
|
453
|
+
const [stale, setStale] = useState(false);
|
|
454
|
+
// useAction resets to idle once the action has settled: keep the notice.
|
|
455
|
+
useEffect(() => {
|
|
456
|
+
if (revalidationError) setStale(true);
|
|
457
|
+
}, [revalidationError]);
|
|
458
|
+
return (
|
|
459
|
+
<form action={saveStock.bind(null, sku)}>
|
|
460
|
+
<button>Save</button>
|
|
461
|
+
{stale && (
|
|
462
|
+
<p role="status">Saved. The stock count could not be refreshed.</p>
|
|
463
|
+
)}
|
|
464
|
+
</form>
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
```
|
|
332
468
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
469
|
+
`StockBoundary` is an ordinary React error boundary whose
|
|
470
|
+
`getDerivedStateFromProps` clears its error when `resetKey` changes. A loader
|
|
471
|
+
that throws `notFound()` or `redirect()` is not a failure and keeps its path:
|
|
472
|
+
a `redirect()` (a deleted record's page) replaces the route's content and
|
|
473
|
+
navigates, and the layouts above keep their instance. A no-JS form post
|
|
474
|
+
renders the whole document after the action, so a loader that throws there
|
|
475
|
+
behaves as on a page load. Every refetch the router runs for an action
|
|
476
|
+
follows the same rule (after several actions at once, after a navigation
|
|
477
|
+
during it), and the action that ran it reports the error as its
|
|
478
|
+
`revalidationError`; with several actions at once, that is the one whose
|
|
479
|
+
refetch covers them all. Not covered yet: an `intercept()`'s loaders, and a
|
|
480
|
+
parallel slot's loaders under its own `loading()`, decoded above the slot's
|
|
481
|
+
content, so a boundary inside the slot does not see the error.
|
|
482
|
+
|
|
483
|
+
For a failure the user should see in place of the data without an error
|
|
484
|
+
boundary (a flaky upstream, a
|
|
485
|
+
rate limit), return it as data instead of throwing, and render it where the
|
|
486
|
+
data goes:
|
|
336
487
|
|
|
337
488
|
```tsx
|
|
338
489
|
// stock.loader.ts
|
|
@@ -355,10 +506,7 @@ function Stock() {
|
|
|
355
506
|
}
|
|
356
507
|
```
|
|
357
508
|
|
|
358
|
-
The action
|
|
359
|
-
next action or navigation that reruns the loader replaces the message. Keep
|
|
360
|
-
throwing for a failure that should stop the page, with an `errorBoundary()`
|
|
361
|
-
around the mount for it.
|
|
509
|
+
The next action or navigation that reruns the loader replaces the message.
|
|
362
510
|
|
|
363
511
|
A failure returned as data is an ordinary value, so whatever stores the
|
|
364
512
|
loader's value stores it too: a `cache()` on the loader serves it for its TTL,
|
|
@@ -370,8 +518,9 @@ data in a loader that is not cached itself.
|
|
|
370
518
|
|
|
371
519
|
Pinned by dev + production e2e in the router repository (a group, its `urls()`
|
|
372
520
|
twin, each with and without `loading()`, and the group under an
|
|
373
|
-
`errorBoundary()`), and by
|
|
374
|
-
|
|
521
|
+
`errorBoundary()`), and by `serveShellRequest({ action })` tests whose
|
|
522
|
+
`readAction()` reads the call's answer as the browser does: the result and
|
|
523
|
+
the `revalidationError`.
|
|
375
524
|
|
|
376
525
|
## Loaders are full citizens: signals and handles
|
|
377
526
|
|
|
@@ -386,7 +535,7 @@ import { createLoader, notFound, redirect } from "@rangojs/router";
|
|
|
386
535
|
|
|
387
536
|
export const ProductLoader = createLoader(async (ctx) => {
|
|
388
537
|
const moved = LEGACY_SLUGS[ctx.params.slug];
|
|
389
|
-
if (moved) throw redirect(
|
|
538
|
+
if (moved) throw redirect(ctx.reverse("shop.product", { slug: moved }));
|
|
390
539
|
if (!(await exists(ctx.params.slug))) notFound(`No "${ctx.params.slug}"`);
|
|
391
540
|
return getProduct(ctx.params.slug);
|
|
392
541
|
});
|
|
@@ -507,7 +656,9 @@ rest of the destination; otherwise it is held. A same-route click
|
|
|
507
656
|
that layout is new to the page (entering the group from outside).
|
|
508
657
|
- `loading()` in `clientUrls()` takes no options, and a flagged `loading()`
|
|
509
658
|
on a server layout above the group defers nothing for the group's routes.
|
|
510
|
-
- `intercept()`
|
|
659
|
+
- On an `intercept()` loader the flag works as in `urls()`: an in-group
|
|
660
|
+
prefetch of the intercept's target defers it, and the click opens the modal
|
|
661
|
+
at once with one fill for it.
|
|
511
662
|
- With `ssr: false` on a `ppr` group route the flag is ignored (the bake
|
|
512
663
|
lane), as on a server route.
|
|
513
664
|
- Test it with `serveShellRequest` in the Flight project: mount the module
|
|
@@ -557,15 +708,27 @@ a hard load of the target URL renders the full route.
|
|
|
557
708
|
hold; it adds the view-transition animation. Inline `<Suspense>` above each
|
|
558
709
|
`useLoader` read is still the finer-grained tool when different reads on
|
|
559
710
|
one route should wait independently.
|
|
711
|
+
- **Parameterized mounts.** A group under `include("/:locale", ...)`,
|
|
712
|
+
`include("/:locale?", ...)` or `include("/:accountId/:projectId", ...)`
|
|
713
|
+
presents at the click like one under a literal mount, and `useParams()` in
|
|
714
|
+
the destination carries the mount's params. A click that changes a mount
|
|
715
|
+
param (`/en/...` to `/de/...`) is another instance of the include: no
|
|
716
|
+
optimistic frame, a plain navigation. The group's intercepts open only from
|
|
717
|
+
one of its routes in the same mount instance. A URL the server routes
|
|
718
|
+
elsewhere (a root route that a group `/:slug` under `/:locale?` also
|
|
719
|
+
matches) never renders as a group route: the server ships the competing
|
|
720
|
+
routes with the group.
|
|
560
721
|
- **Two parallel loaders with equal latency look "SSR'd" together.** Loaders
|
|
561
722
|
kick off in parallel, so awaiting one (`ssr: false`) gives
|
|
562
723
|
same-or-faster siblings time to settle coincidentally. Do not read "it was
|
|
563
724
|
in the HTML once" as a guarantee — only the flagged loader is guaranteed.
|
|
564
725
|
- **Hook semantics shift inside a group.** `usePathname` is ABSOLUTE (mount
|
|
565
726
|
included) — never compare it to your own `path()` patterns; build local
|
|
566
|
-
links with `
|
|
567
|
-
`
|
|
568
|
-
|
|
727
|
+
links with `useReverse(routes)` from the group's `.gen.ts`: it composes the
|
|
728
|
+
mount and fills its params. `useHref()` (`groupHref("/items/1")`) and a
|
|
729
|
+
relative `useRouter().push("cart")` also fill `:param` mount segments from
|
|
730
|
+
the current params (a missing one throws, or rejects the push), but append
|
|
731
|
+
the rest unchecked. `push("/x")` stays
|
|
569
732
|
app-absolute by design (never auto-prefixed). `useSearchParams` carries REAL
|
|
570
733
|
values during SSR (live request seeds the SSR store; on ppr routes the
|
|
571
734
|
capture/resume renders seed the shell key's own sorted search — search is
|
|
@@ -214,14 +214,16 @@ export const shopPatterns = urls(({ path, include }) => [
|
|
|
214
214
|
export default shopPatterns; // async include() resolves this
|
|
215
215
|
```
|
|
216
216
|
|
|
217
|
-
**
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
217
|
+
**Default to the eager (sync) form.** Reach for the async form when a group is
|
|
218
|
+
large and rarely visited, and is a natural, independently-loadable unit (a big
|
|
219
|
+
localized section, a heavy admin area, an API surface with heavy handlers): it
|
|
220
|
+
trims the eagerly-parsed entry bundle and keeps that subgraph off the cold-start
|
|
221
|
+
path, at the cost of the group's first request paying its chunk import (see the
|
|
222
|
+
measurements below). A thin group, or one that shares most of its module graph
|
|
223
|
+
with the entry, gains little from splitting, since the bundler keeps shared
|
|
224
|
+
modules common regardless. Both match identically at runtime — only the module's
|
|
225
|
+
runtime evaluation timing differs. `/app-structure` shows both in whole-app
|
|
226
|
+
layouts.
|
|
225
227
|
|
|
226
228
|
What you do NOT lose by splitting: build-time discovery `await`s the provider, so
|
|
227
229
|
`href()`, `ctx.reverse()`, generated route types, and prerender still see every route
|
|
@@ -51,14 +51,14 @@ Now `ProductPage` carries its loader, loading state, and response-header middlew
|
|
|
51
51
|
|
|
52
52
|
`handler.use()` is the same callback shape regardless of where the handler runs, but the runtime validates that the items it returns are valid for the mount site. Driven by `MOUNT_SITE_ALLOWED_TYPES` in [resolve-handler-use.ts](../../src/route-definition/resolve-handler-use.ts):
|
|
53
53
|
|
|
54
|
-
| Mount site | Allowed item types
|
|
55
|
-
| ------------------------------------------------- |
|
|
56
|
-
| `path()` | `layout`, `parallel`, `intercept`, `middleware`, `revalidate`, `loader`, `loading`, `errorBoundary`, `notFoundBoundary`, `cache`, `transition`
|
|
57
|
-
| `layout()` | All of the above, plus `route` (nested `path()` items) and `include`
|
|
58
|
-
| `parallel()` (per slot) | `revalidate`, `loader`, `loading`, `errorBoundary`, `notFoundBoundary`, `transition`
|
|
59
|
-
| `intercept()` | `
|
|
60
|
-
| Response routes (`path.json()`, `path.text()`, …) | `middleware`, `cache`
|
|
61
|
-
| `loader()` (a `createLoader()` definition) | `revalidate`, `cache`
|
|
54
|
+
| Mount site | Allowed item types |
|
|
55
|
+
| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
56
|
+
| `path()` | `layout`, `parallel`, `intercept`, `middleware`, `revalidate`, `loader`, `loading`, `errorBoundary`, `notFoundBoundary`, `cache`, `transition` |
|
|
57
|
+
| `layout()` | All of the above, plus `route` (nested `path()` items) and `include` |
|
|
58
|
+
| `parallel()` (per slot) | `revalidate`, `loader`, `loading`, `errorBoundary`, `notFoundBoundary`, `transition` |
|
|
59
|
+
| `intercept()` | `loader`, `loading`, `layout` (modal chrome, without its own `use()` items), `route`, `transition`. Conditional activation is config-only: `intercept(..., { when })`. Anything else throws with a pointer: `middleware` goes on the target route (it runs for the modal too), `revalidate` on the intercept's loader (`loader(Def, () => [revalidate(...)])`), `errorBoundary`/`notFoundBoundary` on the enclosing layout, `cache` on the target route |
|
|
60
|
+
| Response routes (`path.json()`, `path.text()`, …) | `middleware`, `cache` |
|
|
61
|
+
| `loader()` (a `createLoader()` definition) | `revalidate`, `cache` |
|
|
62
62
|
|
|
63
63
|
`route` is the item type a `path()` call produces.
|
|
64
64
|
|
|
@@ -292,7 +292,7 @@ AdminLayout.use = () => [
|
|
|
292
292
|
|
|
293
293
|
### Intercepts (`intercept()`)
|
|
294
294
|
|
|
295
|
-
Intercept handlers can carry their own
|
|
295
|
+
Intercept handlers can carry their own loaders, `loading()`, and the modal chrome: a `layout(Chrome)` with no `use()` items of its own. `middleware()`, boundaries, `cache()` and `revalidate()` throw here (see the table above and [intercept](../intercept/SKILL.md)); the target route's middleware runs for the modal.
|
|
296
296
|
|
|
297
297
|
```typescript
|
|
298
298
|
const QuickViewModal: Handler = async (ctx) => {
|
|
@@ -392,6 +392,44 @@ Rule of thumb: only put `loading()` in `handler.use` if you genuinely want every
|
|
|
392
392
|
- **Validation runs at registration / first match**, not at handler definition. A handler doesn't know its mount site at definition time — the same handler used in a `path()` and an `intercept()` is validated against each mount's allowed-types set when registered.
|
|
393
393
|
- **No silent shadowing.** If a disallowed item slips through (e.g., a layout factory returning `cache()` from a slot's `handler.use`), the runtime throws with the offending type and mount site named.
|
|
394
394
|
|
|
395
|
+
## In `clientUrls()`: `Component.use`
|
|
396
|
+
|
|
397
|
+
A client route group (`/client-urls`) takes the same pattern for its
|
|
398
|
+
components: `Dashboard.use = () => [...]` on a page, layout or intercept
|
|
399
|
+
component mounted by the group's `path()`, `layout()` or `intercept()`. Build
|
|
400
|
+
the items with the helpers `@rangojs/router/client` exports (`loader`,
|
|
401
|
+
`loading`, `revalidate`, `transition`); the `@rangojs/router` ones are the
|
|
402
|
+
server DSL's and throw in a client module.
|
|
403
|
+
|
|
404
|
+
```tsx
|
|
405
|
+
"use client";
|
|
406
|
+
import { loader, loading, revalidate } from "@rangojs/router/client";
|
|
407
|
+
|
|
408
|
+
Dashboard.use = () => [
|
|
409
|
+
loader(StatsLoader, () => [
|
|
410
|
+
revalidate(({ isAction }) => isAction(StatsActions)),
|
|
411
|
+
]),
|
|
412
|
+
loading(<DashboardSkeleton />),
|
|
413
|
+
];
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
The composition rules are this skill's: the component's items first, the
|
|
417
|
+
mount site's explicit items second, `loading()` and `transition()` last-wins,
|
|
418
|
+
loaders accumulate. What differs:
|
|
419
|
+
|
|
420
|
+
| Mount site in `clientUrls()` | The component's `.use` may return |
|
|
421
|
+
| ---------------------------- | ------------------------------------------ |
|
|
422
|
+
| `path()` | `loader`, `loading`, `transition` |
|
|
423
|
+
| `layout()` | `loader`, `loading` |
|
|
424
|
+
| `intercept()` | `loader` (without `ssr: false`), `loading` |
|
|
425
|
+
|
|
426
|
+
- No `route`, `layout`, `intercept` or `include` items: a group's routes and
|
|
427
|
+
its generated types come from the builder's own items.
|
|
428
|
+
- A `revalidate()` predicate still runs on the server: the build hoists it
|
|
429
|
+
out of the component's module (see `/client-urls`, "Revalidation runs on
|
|
430
|
+
the server").
|
|
431
|
+
- A `createLoader()` definition's `.use` is not read inside a group yet.
|
|
432
|
+
|
|
395
433
|
## Cross-references
|
|
396
434
|
|
|
397
435
|
- `skills/route` — `path()` mount site basics
|
|
@@ -400,3 +438,4 @@ Rule of thumb: only put `loading()` in `handler.use` if you genuinely want every
|
|
|
400
438
|
- `skills/intercept` — intercept mount site basics
|
|
401
439
|
- `skills/loader` — defining `createLoader` and reading via `ctx.use()`
|
|
402
440
|
- `skills/middleware` — middleware semantics and ordering
|
|
441
|
+
- `skills/client-urls` — `Component.use` in a client route group
|
|
@@ -39,6 +39,107 @@ the capture, so its pushes always apply client-side; with `ssr: false` they
|
|
|
39
39
|
bake into the shell. See `/loader` → "Writing Handles from Loaders". `useHandle`
|
|
40
40
|
re-renders on every handle update, late loader pushes included.
|
|
41
41
|
|
|
42
|
+
### When handle values change (by design)
|
|
43
|
+
|
|
44
|
+
Handle values are reactive and arrive on their own channel, by design. A value
|
|
45
|
+
belongs to the segment that pushed it and reaches `useHandle` when that push
|
|
46
|
+
settles. The page never waits for a handle, and a handle never shows the next
|
|
47
|
+
page's values over the page being left:
|
|
48
|
+
|
|
49
|
+
| Case | What `useHandle` returns |
|
|
50
|
+
| ------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
|
|
51
|
+
| Document load | The values the HTML was rendered from. `push(promise)` and `.defer()` are awaited during SSR, so the document ships the resolved value. |
|
|
52
|
+
| Navigation to a page that commits at once (a cold click that mounts new segments) | The destination's values, in the page's commit; the page's effects read them |
|
|
53
|
+
| Navigation to a page React holds in a transition (`transition()`, same-route search change, fully prefetched click, a handler that awaits) | The page being left keeps its values; the destination's land in the commit right after the page, in the same task, before paint |
|
|
54
|
+
| `push(promise)` or `.defer()` on a client navigation | The previous value, then the new one when the promise resolves, possibly well after the page |
|
|
55
|
+
|
|
56
|
+
You never see a pending value, a blank, or the destination's title over the old
|
|
57
|
+
page. A crumb whose `content` is a promise read with `use()` in `<Suspense>`
|
|
58
|
+
never holds the page either; only that boundary waits for it.
|
|
59
|
+
|
|
60
|
+
**What this means for effects.** An effect keyed only on the URL
|
|
61
|
+
(`usePathname()`) can run before the destination's values arrive and read the
|
|
62
|
+
previous page's value. That is the contract, not a bug. When a reader must tie a
|
|
63
|
+
handle value to a page (an analytics `page_view`), put the page's path in the
|
|
64
|
+
handle value and key the effect on the value, deduped by path:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
// handles/page-view.ts
|
|
68
|
+
import { createHandle } from "@rangojs/router";
|
|
69
|
+
|
|
70
|
+
export interface PageView {
|
|
71
|
+
path: string;
|
|
72
|
+
title?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Collect the item, not the array of segment arrays.
|
|
76
|
+
export const PageView = createHandle<PageView, PageView | undefined>(
|
|
77
|
+
(segments) => segments.flat().at(-1),
|
|
78
|
+
);
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
// urls: a root slot that re-runs on every navigation
|
|
83
|
+
parallel(
|
|
84
|
+
{
|
|
85
|
+
"@pageView": (ctx) => {
|
|
86
|
+
ctx.use(PageView)({ path: ctx.pathname + ctx.url.search });
|
|
87
|
+
return null;
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
() => [revalidate(true)],
|
|
91
|
+
);
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
"use client";
|
|
96
|
+
import { useEffect, useRef } from "react";
|
|
97
|
+
import { useHandle } from "@rangojs/router/client";
|
|
98
|
+
import { PageView } from "./handles/page-view";
|
|
99
|
+
|
|
100
|
+
export function PageViews() {
|
|
101
|
+
const pv = useHandle(PageView);
|
|
102
|
+
// Seeded with the landing path: the first page_view is sent elsewhere (a
|
|
103
|
+
// bootstrap script). Start from `null` to send it from here instead. The
|
|
104
|
+
// component also renders on the server, where there is no `window`.
|
|
105
|
+
const last = useRef(
|
|
106
|
+
typeof window === "undefined"
|
|
107
|
+
? null
|
|
108
|
+
: window.location.pathname + window.location.search,
|
|
109
|
+
);
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
if (!pv || pv.path === last.current) return;
|
|
112
|
+
last.current = pv.path;
|
|
113
|
+
gtag("event", "page_view", { page_path: pv.path, page_title: pv.title });
|
|
114
|
+
}, [pv]);
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The effect fires once per page, because the value carries its own path: the
|
|
120
|
+
previous page's value has the previous path and is skipped, and the destination's
|
|
121
|
+
fires when it arrives. Trade-offs:
|
|
122
|
+
|
|
123
|
+
- Dedupe is by path. Include `ctx.url.search` in `path` if search-only
|
|
124
|
+
navigations count; a same-URL refresh or an action's revalidation re-delivers
|
|
125
|
+
an equal value and sends nothing. Under dev StrictMode the mount effect runs
|
|
126
|
+
twice; the path check covers it.
|
|
127
|
+
- Select or collect the item, not an array: `useHandle` keeps its reference
|
|
128
|
+
while the value is equal one level deep, and an array of objects is rebuilt on
|
|
129
|
+
every delivery (an action's included). With the default collector, use
|
|
130
|
+
`useHandle(PageView, (s) => s.at(-1)?.at(-1))`.
|
|
131
|
+
- Async values arrive later. With `push(promise)` the page_view fires when the
|
|
132
|
+
promise resolves, with the resolved value; read the title from the value, not
|
|
133
|
+
from `document.title`.
|
|
134
|
+
- A page that pushes nothing under its own `PageView` keeps the previous value
|
|
135
|
+
and fires nothing for itself; the root slot above stamps the path on every
|
|
136
|
+
navigation so every route counts.
|
|
137
|
+
- Back/Forward fires (the entry's path differs from the current one).
|
|
138
|
+
|
|
139
|
+
A server component that pushes after an `await` throws `LateHandlePushError`:
|
|
140
|
+
reserve the slot in the handler with `ctx.use(Handle).defer()` and resolve it
|
|
141
|
+
from the component (`/breadcrumbs`, "Deferred content").
|
|
142
|
+
|
|
42
143
|
On a document load `useHandle` hydrates with the handle data its HTML was
|
|
43
144
|
rendered from, wherever the reader sits; late loader pushes reach every reader
|
|
44
145
|
once the document has finished streaming and every boundary has hydrated (see
|
|
@@ -107,6 +208,9 @@ function AddToCartButton({ productId }: { productId: string }) {
|
|
|
107
208
|
// payload: unknown[] | FormData | null (the call's arguments)
|
|
108
209
|
// error: unknown | null (whatever the action threw)
|
|
109
210
|
// result: unknown | null (return value)
|
|
211
|
+
// revalidationError: unknown | null (a loader that threw while it reran
|
|
212
|
+
// after the action; the action succeeded and the loader threw at its
|
|
213
|
+
// read: /client-urls -> "A loader that fails on its rerun")
|
|
110
214
|
|
|
111
215
|
return (
|
|
112
216
|
// .bind keeps the form progressively enhanced; useAction still matches it
|
|
@@ -35,15 +35,23 @@ on screen:
|
|
|
35
35
|
|
|
36
36
|
| What you want to show | Read |
|
|
37
37
|
| ---------------------------------------------- | ----------------------------------------------------------------------------------- |
|
|
38
|
-
| a request is out, nothing has arrived | `
|
|
38
|
+
| a request is out, nothing has arrived | `pendingUrl` is the URL being fetched; `null` once the response arrives |
|
|
39
|
+
| the new page is not on screen yet | `state === "loading"`; `"idle"` commits together with the new page and its URL |
|
|
39
40
|
| the page has not fully arrived yet | `isStreaming`: true from the start of the request until the stream ends |
|
|
40
41
|
| which page is on screen while the request runs | `location`, or `usePathname()` / `useSearchParams()` (see [`./urls.md`](./urls.md)) |
|
|
41
42
|
|
|
42
|
-
`
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
`pendingUrl` covers the network wait. Once the response has arrived a loader
|
|
44
|
+
may still be streaming, and React keeps the page being left on screen where a
|
|
45
|
+
navigation holds its content (a same-route change, a `transition()` route,
|
|
46
|
+
with or without a view transition). `state` stays `"loading"` through that
|
|
47
|
+
part: for a navigation on its own, `"idle"` commits in the same commit as the
|
|
48
|
+
destination's `useSearchParams()`, `usePathname()` and `useParams()`, so a
|
|
49
|
+
component that reads both does not see the navigation finished on the URL it
|
|
50
|
+
is leaving. A server action, or another navigation started and aborted,
|
|
51
|
+
while React holds that page can still bring `"idle"` with the URL being left.
|
|
52
|
+
`isStreaming` can outlast that commit (a loader still streaming below a
|
|
53
|
+
boundary that already showed), so use it for a progress indicator that should
|
|
54
|
+
last until the new page is complete.
|
|
47
55
|
|
|
48
56
|
On Back/Forward the address bar changes at once, before any of this: during
|
|
49
57
|
the wait `window.location` is the entry being fetched, the same URL as
|
|
@@ -51,9 +59,9 @@ the wait `window.location` is the entry being fetched, the same URL as
|
|
|
51
59
|
from the client's history cache restores its page in one commit and reports
|
|
52
60
|
no pending state.
|
|
53
61
|
|
|
54
|
-
`location` moves
|
|
55
|
-
|
|
56
|
-
|
|
62
|
+
`location` moves in that commit too. A component that first mounts while
|
|
63
|
+
React holds the page being left reads the destination's `location` and URL
|
|
64
|
+
hooks (issue #1046).
|
|
57
65
|
|
|
58
66
|
### useRouter()
|
|
59
67
|
|
|
@@ -103,7 +111,10 @@ structured-clone-safe data; see [`./state.md`](./state.md)).
|
|
|
103
111
|
Target resolution: a path starting with `/` is app-absolute and gets the
|
|
104
112
|
router `basename` prefixed. A relative path (`"cart"`, `"./cart"`) resolves
|
|
105
113
|
against the current `include()` mount, so `router.push("cart")` inside
|
|
106
|
-
`include("/shop", ...)` navigates to `/shop/cart`.
|
|
114
|
+
`include("/shop", ...)` navigates to `/shop/cart`. A parameterized mount is
|
|
115
|
+
filled from the current route params: inside `include("/:accountId", ...)` at
|
|
116
|
+
`/acme/board`, `router.push("cart")` navigates to `/acme/cart`; a missing
|
|
117
|
+
mount param rejects the returned promise.
|
|
107
118
|
|
|
108
119
|
#### Skipping revalidation
|
|
109
120
|
|
|
@@ -176,6 +187,9 @@ const segments = useSegments();
|
|
|
176
187
|
const part = segments.path[depth];
|
|
177
188
|
```
|
|
178
189
|
|
|
190
|
+
Like `useHandle`, it commits with the page: while React holds the page being
|
|
191
|
+
left, `useSegments()` keeps that page's path and segments.
|
|
192
|
+
|
|
179
193
|
### useLinkStatus()
|
|
180
194
|
|
|
181
195
|
Track pending state inside a Link component. `pending` is `true` while a
|