@rangojs/router 0.5.2 → 0.7.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/dist/bin/rango.js +343 -125
- package/dist/types/browser/react/use-router.d.ts +10 -3
- package/dist/types/browser/react/use-search-params.d.ts +57 -10
- package/dist/types/browser/types.d.ts +22 -0
- package/dist/types/build/merge-full-manifests.d.ts +3 -0
- package/dist/types/build/route-trie.d.ts +4 -73
- package/dist/types/build/route-types/per-module-writer.d.ts +6 -4
- package/dist/types/build/route-types/router-processing.d.ts +2 -3
- package/dist/types/cache/cache-exec-scope.d.ts +31 -0
- package/dist/types/cache/taint.d.ts +12 -6
- package/dist/types/client-urls/client-root.d.ts +38 -0
- package/dist/types/client-urls/client-urls.d.ts +5 -0
- package/dist/types/client-urls/navigation.d.ts +38 -0
- package/dist/types/client-urls/revalidation-protocol.d.ts +25 -0
- package/dist/types/client-urls/server-projection.d.ts +71 -0
- package/dist/types/client-urls/types.d.ts +147 -0
- package/dist/types/client.d.ts +12 -4
- package/dist/types/client.rsc.d.ts +4 -1
- package/dist/types/decode-loader-results.d.ts +37 -0
- package/dist/types/errors.d.ts +1 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/loader-redirect.d.ts +27 -0
- package/dist/types/outlet-context.d.ts +12 -0
- package/dist/types/outlet-provider.d.ts +3 -1
- package/dist/types/redirect-origin.d.ts +4 -0
- package/dist/types/route-content-wrapper.d.ts +42 -1
- package/dist/types/route-definition/helpers-types.d.ts +13 -2
- package/dist/types/router/error-handling.d.ts +35 -1
- package/dist/types/router/intercept-resolution.d.ts +12 -0
- package/dist/types/router/loader-resolution.d.ts +24 -2
- package/dist/types/router/revalidation.d.ts +7 -0
- package/dist/types/router/route-trie-builder.d.ts +77 -0
- package/dist/types/router/router-interfaces.d.ts +20 -0
- package/dist/types/router/segment-resolution/helpers.d.ts +1 -1
- package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -9
- package/dist/types/router/trie-matching.d.ts +1 -1
- package/dist/types/rsc/manifest-init.d.ts +5 -5
- package/dist/types/rsc/shell-capture.d.ts +9 -0
- package/dist/types/rsc/shell-serve.d.ts +11 -0
- package/dist/types/rsc/types.d.ts +30 -0
- package/dist/types/segment-system.d.ts +2 -0
- package/dist/types/server/context.d.ts +10 -0
- package/dist/types/server/handle-store.d.ts +34 -3
- package/dist/types/server/request-context.d.ts +11 -1
- package/dist/types/server.d.ts +1 -0
- package/dist/types/ssr/index.d.ts +22 -0
- package/dist/types/ssr/ssr-root.d.ts +10 -0
- package/dist/types/testing/dom.entry.d.ts +1 -1
- package/dist/types/testing/render-route.d.ts +16 -6
- package/dist/types/testing/run-loader.d.ts +9 -0
- package/dist/types/types/boundaries.d.ts +22 -0
- package/dist/types/types/index.d.ts +1 -1
- package/dist/types/types/loader-types.d.ts +57 -5
- package/dist/types/types/segments.d.ts +7 -0
- package/dist/types/urls/path-helper-types.d.ts +13 -4
- package/dist/types/vite/discovery/client-urls-projection.d.ts +53 -0
- package/dist/types/vite/discovery/discover-routers.d.ts +1 -1
- package/dist/types/vite/discovery/state.d.ts +8 -1
- package/dist/types/vite/plugins/client-ref-dedup.d.ts +0 -11
- package/dist/vite/index.js +6159 -2959
- package/package.json +6 -5
- package/skills/breadcrumbs/SKILL.md +39 -9
- package/skills/catalog.json +7 -1
- package/skills/client-urls/SKILL.md +338 -0
- package/skills/comparison/references/framework-comparison.md +23 -9
- package/skills/hooks/SKILL.md +2 -2
- package/skills/hooks/data.md +11 -2
- package/skills/hooks/handle-and-actions.md +7 -0
- package/skills/hooks/outlets.md +26 -5
- package/skills/hooks/urls.md +40 -3
- package/skills/loader/SKILL.md +132 -20
- package/skills/migrate-nextjs/SKILL.md +70 -10
- package/skills/migrate-react-router/SKILL.md +49 -13
- package/skills/migrate-react-router/component-migration.md +18 -13
- package/skills/migrate-react-router/data-and-actions.md +14 -3
- package/skills/migrate-react-router/route-mapping.md +15 -2
- package/skills/parallel/SKILL.md +32 -1
- package/skills/ppr/SKILL.md +16 -6
- package/skills/prerender/SKILL.md +8 -4
- package/skills/rango/SKILL.md +21 -17
- package/skills/react-compiler/SKILL.md +3 -3
- package/skills/route/SKILL.md +5 -2
- package/skills/router-setup/SKILL.md +16 -2
- package/skills/scripts/SKILL.md +16 -6
- package/skills/shell-manifest/SKILL.md +16 -7
- package/skills/testing/SKILL.md +2 -2
- package/skills/testing/client-components.md +6 -0
- package/skills/testing/handles.md +30 -8
- package/skills/testing/loader.md +51 -49
- package/skills/testing/middleware.md +1 -1
- package/skills/theme/SKILL.md +8 -5
- package/src/bin/rango.ts +7 -3
- package/src/browser/navigation-bridge.ts +6 -0
- package/src/browser/navigation-client.ts +5 -0
- package/src/browser/partial-update.ts +65 -13
- package/src/browser/react/use-router.ts +40 -11
- package/src/browser/react/use-search-params.ts +140 -17
- package/src/browser/rsc-router.tsx +59 -0
- package/src/browser/server-action-bridge.ts +26 -0
- package/src/browser/types.ts +22 -0
- package/src/build/merge-full-manifests.ts +161 -0
- package/src/build/route-trie.ts +9 -332
- package/src/build/route-types/include-resolution.ts +66 -11
- package/src/build/route-types/per-module-writer.ts +11 -6
- package/src/build/route-types/router-processing.ts +184 -153
- package/src/build/runtime-discovery.ts +23 -12
- package/src/cache/cache-exec-scope.ts +47 -0
- package/src/cache/cache-runtime.ts +24 -25
- package/src/cache/taint.ts +28 -9
- package/src/client-urls/client-root.tsx +168 -0
- package/src/client-urls/client-urls.ts +776 -0
- package/src/client-urls/navigation.ts +237 -0
- package/src/client-urls/revalidation-protocol.ts +56 -0
- package/src/client-urls/server-projection.ts +670 -0
- package/src/client-urls/types.ts +201 -0
- package/src/client.rsc.tsx +12 -0
- package/src/client.tsx +49 -6
- package/src/decode-loader-results.ts +113 -0
- package/src/errors.ts +14 -0
- package/src/handles/deferred-resolution.ts +14 -7
- package/src/index.ts +1 -0
- package/src/loader-redirect.tsx +64 -0
- package/src/outlet-context.ts +12 -0
- package/src/outlet-provider.tsx +15 -1
- package/src/redirect-origin.ts +29 -0
- package/src/route-content-wrapper.tsx +96 -3
- package/src/route-definition/dsl-helpers.ts +28 -3
- package/src/route-definition/helpers-types.ts +13 -0
- package/src/route-definition/redirect.ts +17 -18
- package/src/router/error-handling.ts +65 -11
- package/src/router/intercept-resolution.ts +29 -0
- package/src/router/loader-resolution.ts +261 -28
- package/src/router/match-result.ts +7 -0
- package/src/router/revalidation.ts +24 -11
- package/src/router/route-trie-builder.ts +334 -0
- package/src/router/router-interfaces.ts +38 -0
- package/src/router/segment-resolution/fresh.ts +55 -2
- package/src/router/segment-resolution/helpers.ts +9 -11
- package/src/router/segment-resolution/loader-cache.ts +34 -23
- package/src/router/segment-resolution/loader-mask.ts +9 -9
- package/src/router/segment-resolution/revalidation.ts +23 -2
- package/src/router/trie-matching.ts +3 -3
- package/src/router.ts +46 -1
- package/src/rsc/full-payload.ts +6 -0
- package/src/rsc/handler.ts +10 -7
- package/src/rsc/loader-fetch.ts +2 -2
- package/src/rsc/manifest-init.ts +28 -9
- package/src/rsc/rsc-rendering.ts +15 -1
- package/src/rsc/shell-capture.ts +12 -0
- package/src/rsc/shell-serve.ts +15 -2
- package/src/rsc/ssr-setup.ts +10 -1
- package/src/rsc/types.ts +31 -2
- package/src/segment-system.tsx +83 -26
- package/src/server/context.ts +10 -0
- package/src/server/cookie-store.ts +19 -19
- package/src/server/handle-store.ts +185 -48
- package/src/server/request-context.ts +30 -6
- package/src/server.ts +7 -0
- package/src/ssr/index.tsx +37 -2
- package/src/ssr/ssr-root.tsx +29 -2
- package/src/testing/dom.entry.ts +1 -1
- package/src/testing/render-route.tsx +22 -8
- package/src/testing/run-loader.ts +51 -13
- package/src/types/boundaries.ts +19 -0
- package/src/types/index.ts +1 -0
- package/src/types/loader-types.ts +60 -5
- package/src/types/segments.ts +7 -0
- package/src/urls/include-helper.ts +22 -4
- package/src/urls/path-helper-types.ts +17 -1
- package/src/use-loader.tsx +67 -6
- package/src/vite/discovery/client-urls-projection.ts +322 -0
- package/src/vite/discovery/discover-routers.ts +43 -17
- package/src/vite/discovery/state.ts +11 -1
- package/src/vite/discovery/virtual-module-codegen.ts +20 -0
- package/src/vite/plugins/client-ref-dedup.ts +281 -19
- package/src/vite/plugins/expose-action-id.ts +45 -23
- package/src/vite/plugins/virtual-entries.ts +12 -3
- package/src/vite/router-discovery.ts +163 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rangojs/router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Django-inspired RSC router with composable URL patterns",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -182,6 +182,7 @@
|
|
|
182
182
|
"prepublishOnly": "pnpm build",
|
|
183
183
|
"typecheck": "tsc --noEmit && tsc -p tsconfig.strict-check.json --noEmit && tsc -p tsconfig.augment-check.json --noEmit",
|
|
184
184
|
"test": "playwright test",
|
|
185
|
+
"test:preload": "playwright test --config=playwright.preload.config.ts",
|
|
185
186
|
"test:ui": "playwright test --ui",
|
|
186
187
|
"test:hmr-local": "playwright test --project=dev-warmup --project=hmr-basename --project=hmr-prerender --no-deps --workers=1 && RANGO_E2E_ROUTE_HMR_ONLY=1 playwright test --project=hmr-routes --no-deps --workers=1",
|
|
187
188
|
"test:hmr-routes-local": "RANGO_E2E_ROUTE_HMR_ONLY=1 playwright test --project=hmr-routes --no-deps --workers=1",
|
|
@@ -191,7 +192,7 @@
|
|
|
191
192
|
},
|
|
192
193
|
"dependencies": {
|
|
193
194
|
"@types/debug": "^4.1.12",
|
|
194
|
-
"@vitejs/plugin-rsc": "^0.5.
|
|
195
|
+
"@vitejs/plugin-rsc": "^0.5.31",
|
|
195
196
|
"debug": "^4.4.1",
|
|
196
197
|
"magic-string": "^0.30.17",
|
|
197
198
|
"picomatch": "^4.0.4",
|
|
@@ -224,9 +225,9 @@
|
|
|
224
225
|
"@playwright/test": "^1.49.1",
|
|
225
226
|
"@testing-library/react": ">=16",
|
|
226
227
|
"@vercel/functions": "^3.0.0",
|
|
227
|
-
"@vitejs/plugin-rsc": "^0.5.
|
|
228
|
-
"react": ">=19.2.
|
|
229
|
-
"react-dom": ">=19.2.
|
|
228
|
+
"@vitejs/plugin-rsc": "^0.5.31",
|
|
229
|
+
"react": ">=19.2.8 <20",
|
|
230
|
+
"react-dom": ">=19.2.8 <20",
|
|
230
231
|
"vite": "^8.0.16",
|
|
231
232
|
"vitest": ">=3"
|
|
232
233
|
},
|
|
@@ -7,8 +7,9 @@ argument-hint: "[setup]"
|
|
|
7
7
|
# Breadcrumbs
|
|
8
8
|
|
|
9
9
|
Built-in handle for accumulating breadcrumb items across route segments.
|
|
10
|
-
|
|
11
|
-
collected in parent-to-child order with automatic deduplication by
|
|
10
|
+
Handlers and loader bodies push items via `ctx.use(Breadcrumbs)`, and they
|
|
11
|
+
are collected in parent-to-child order with automatic deduplication by
|
|
12
|
+
`href`.
|
|
12
13
|
|
|
13
14
|
## BreadcrumbItem Type
|
|
14
15
|
|
|
@@ -58,6 +59,32 @@ export const urlpatterns = urls(({ path, layout }) => [
|
|
|
58
59
|
|
|
59
60
|
On `/blog/my-post`, breadcrumbs accumulate: `Home > Blog > my-post`.
|
|
60
61
|
|
|
62
|
+
## Pushing from Loaders
|
|
63
|
+
|
|
64
|
+
A data-derived crumb belongs where the data lives — the loader body pushes
|
|
65
|
+
with the same API (handler parity), so routes without handlers (`useLoader`
|
|
66
|
+
consumption, `clientUrls()` groups) still build trails:
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
export const ProductLoader = createLoader(async (ctx) => {
|
|
70
|
+
"use server";
|
|
71
|
+
const product = await getProduct(ctx.params.slug);
|
|
72
|
+
|
|
73
|
+
const pushCrumb = ctx.use(Breadcrumbs);
|
|
74
|
+
pushCrumb({ label: "Shop", href: "/shop" });
|
|
75
|
+
pushCrumb({ label: product.name, href: `/shop/product/${product.slug}` });
|
|
76
|
+
|
|
77
|
+
return product;
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Two pushes from one segment accumulate in push order (`Shop › product`).
|
|
82
|
+
Delivery follows the loader race model: a push that beats the handler barrier
|
|
83
|
+
is in the SSR'd document; a push after a slow fetch streams and applies
|
|
84
|
+
client-side (`useHandle` re-renders when it lands). To guarantee document
|
|
85
|
+
delivery, register the loader as `loader(Def, { stream: "navigation" })` —
|
|
86
|
+
see `/loader` → "Writing Handles from Loaders".
|
|
87
|
+
|
|
61
88
|
## Async Content
|
|
62
89
|
|
|
63
90
|
The `content` field supports `Promise<ReactNode>` for streaming:
|
|
@@ -83,9 +110,10 @@ with React's `use()` hook wrapped in `<Suspense>`.
|
|
|
83
110
|
|
|
84
111
|
### Deferred content (decide now, resolve from a deep component)
|
|
85
112
|
|
|
86
|
-
When the handler should DECIDE to push a crumb
|
|
87
|
-
|
|
88
|
-
|
|
113
|
+
When the handler should DECIDE to push a crumb early but the value is produced
|
|
114
|
+
far away — by a deep async component, not the handler — call `.defer()` on the
|
|
115
|
+
push function. (If the value comes from a LOADER, skip `.defer()` entirely and
|
|
116
|
+
push from the loader body — see "Pushing from Loaders" above.)
|
|
89
117
|
`ctx.use(Handle)` returns the push function; `.defer(options)` reserves the crumb's
|
|
90
118
|
slot synchronously and returns a **resolver that is push-equal** — you call it
|
|
91
119
|
later, anywhere in the render, with the same argument you'd have passed to the
|
|
@@ -140,10 +168,12 @@ On a full/SSR load the value is resolved server-side; on a soft navigation the
|
|
|
140
168
|
breadcrumbs HOLD the previous resolved value until the deferred value lands, then
|
|
141
169
|
swap in — no blank, no pending entry. If the slot times out to `else: null`/
|
|
142
170
|
undefined, the entry is simply dropped. Use `.defer()` only when even
|
|
143
|
-
`label`/`href` are unknown at handler time
|
|
144
|
-
|
|
145
|
-
(
|
|
146
|
-
|
|
171
|
+
`label`/`href` are unknown at handler time AND the producer is a component,
|
|
172
|
+
not a loader — a loader-produced crumb pushes directly from the loader body
|
|
173
|
+
(add `{ stream: "navigation" }` if it must be in the SSR'd document), and if
|
|
174
|
+
you know `label`/`href` and only the `content` is async, push a concrete item
|
|
175
|
+
with a `Promise` `content` field instead (the `content` field is a nested
|
|
176
|
+
promise you resolve with `use()` in your component; no `.defer()` needed).
|
|
147
177
|
|
|
148
178
|
## Consuming Breadcrumbs (Client)
|
|
149
179
|
|
package/skills/catalog.json
CHANGED
|
@@ -33,6 +33,12 @@
|
|
|
33
33
|
"argumentHint": "[setup]",
|
|
34
34
|
"path": "skills/caching/SKILL.md"
|
|
35
35
|
},
|
|
36
|
+
{
|
|
37
|
+
"name": "client-urls",
|
|
38
|
+
"description": "Define client-component route groups with clientUrls() in a \"use client\" module — no handlers, useLoader at read sites, client-run revalidate predicates, loader-thrown notFound/redirect, loader handle writes, and the stream:\"navigation\" SSR-completeness opt-in. Use when a route group's pages are client components, when building high-navigation-speed UIs (dashboards, admin panels, settings) where transitions must feel instant, when moving data reads from handler ctx.use() to useLoader at consumption sites, or when asking how routes defined in a \"use client\" file work.",
|
|
39
|
+
"argumentHint": "[setup]",
|
|
40
|
+
"path": "skills/client-urls/SKILL.md"
|
|
41
|
+
},
|
|
36
42
|
{
|
|
37
43
|
"name": "cloudflare",
|
|
38
44
|
"description": "Deploy and operate @rangojs/router on Cloudflare Workers with the Cloudflare Vite plugin, typed D1/KV bindings, local migrations, secrets, streaming, preview parity, and built-worker deployment. Use when creating a Cloudflare Rango app, moving a Rango app to Workers, wiring D1 or KV, debugging dev/preview differences, or preparing a Worker deployment.",
|
|
@@ -131,7 +137,7 @@
|
|
|
131
137
|
},
|
|
132
138
|
{
|
|
133
139
|
"name": "loader",
|
|
134
|
-
"description": "Define data loaders for fetching data in routes with createLoader. Use when pages need per-request data that stays fresh, data should stream while the page renders,
|
|
140
|
+
"description": "Define data loaders for fetching data in routes with createLoader. Use when pages need per-request data that stays fresh, data should stream while the page renders, client components need reactive server data, a loader should throw notFound()/redirect(), set page meta/breadcrumbs from loader data (handle writes), or loader data must be guaranteed in the SSR'd document (stream:\"navigation\").",
|
|
135
141
|
"argumentHint": "[loader]",
|
|
136
142
|
"path": "skills/loader/SKILL.md"
|
|
137
143
|
},
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: client-urls
|
|
3
|
+
description: Define client-component route groups with clientUrls() in a "use client" module — no handlers, useLoader at read sites, client-run revalidate predicates, loader-thrown notFound/redirect, loader handle writes, and the stream:"navigation" SSR-completeness opt-in. Use when a route group's pages are client components, when building high-navigation-speed UIs (dashboards, admin panels, settings) where transitions must feel instant, when moving data reads from handler ctx.use() to useLoader at consumption sites, or when asking how routes defined in a "use client" file work.
|
|
4
|
+
argument-hint: "[setup]"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Client route groups with clientUrls()
|
|
8
|
+
|
|
9
|
+
`clientUrls()` defines a route group inside a `"use client"` module. The
|
|
10
|
+
components are client components, and there are **no handlers at all** — every
|
|
11
|
+
data read is `useLoader` at the consumption site, waiting is scoped by the
|
|
12
|
+
inline `<Suspense>` above each read, and per-loader `revalidate()` predicates
|
|
13
|
+
run in the browser. The server shop pattern — `await ctx.use(ProductLoader)` in
|
|
14
|
+
a handler — is pipeline-blocking by contract: no pending UI can ever cover that
|
|
15
|
+
latency. `clientUrls()` makes that shape unrepresentable.
|
|
16
|
+
|
|
17
|
+
This is the fastest-transition shape Rango has, and the natural fit for
|
|
18
|
+
dashboard / admin / settings-style apps — high navigation frequency inside one
|
|
19
|
+
layout, mostly tab/param/filter switches. Three things compound: the
|
|
20
|
+
definition also matches in the browser, so a soft navigation presents
|
|
21
|
+
optimistic pending UI immediately (`useOutlet().pending`) with no server
|
|
22
|
+
round-trip to start; browser-run `revalidate()` predicates HOLD data across
|
|
23
|
+
navigations that don't invalidate it (a tab switch re-runs nothing — only the
|
|
24
|
+
decision crosses the wire); and any read that does refresh streams behind its
|
|
25
|
+
own inline boundary instead of gating the commit. Add per-Link prefetch
|
|
26
|
+
(`/links`) and a warmed click lands the complete page instantly.
|
|
27
|
+
|
|
28
|
+
## Not this skill if…
|
|
29
|
+
|
|
30
|
+
- You are defining server routes with handlers — that is `urls()`: see
|
|
31
|
+
`/route`.
|
|
32
|
+
- You want loader mechanics (createLoader, caching a loader, fetchable
|
|
33
|
+
loaders) — see `/loader`. This skill covers what is _different_ inside
|
|
34
|
+
`clientUrls()`.
|
|
35
|
+
- You want `useOutlet`/`useLoader`/`useHandle` reference — see `/hooks`.
|
|
36
|
+
|
|
37
|
+
## The shape
|
|
38
|
+
|
|
39
|
+
A `clientUrls()` module never ships route-definition code to the server as
|
|
40
|
+
code. The Vite plugin discovers the module (it must be `"use client"`, contain
|
|
41
|
+
`clientUrls(`, and **default-export** the result), evaluates it, and serializes
|
|
42
|
+
a JSON **projection**: patterns, names, options, loader ids, which loaders are
|
|
43
|
+
flagged, loading/transition presence. The server tree materializes real routes
|
|
44
|
+
from that projection — loader stubs resolved by id from the server loader
|
|
45
|
+
registry, your client components referenced but never executed server-side as
|
|
46
|
+
route code. In the browser the same definition matches locally, so soft
|
|
47
|
+
navigations inside the group present optimistic pending UI
|
|
48
|
+
(`useOutlet().pending`) while the canonical partial Flight settles.
|
|
49
|
+
|
|
50
|
+
Consequences you will hit:
|
|
51
|
+
|
|
52
|
+
- **Components must be named function values.** `path("/x", HomePage)`, not
|
|
53
|
+
`path("/x", <HomePage />)` and not an inline arrow — the projection stores a
|
|
54
|
+
component reference, and an anonymous value throws
|
|
55
|
+
(`clientUrls() path() expects a named client component value`).
|
|
56
|
+
- **Loaders must be `createLoader()` definitions** whose bodies are
|
|
57
|
+
`"use server"` — they execute on the server, addressed by id; the client
|
|
58
|
+
module only imports the definition object.
|
|
59
|
+
- **Everything in the definition must be JSON-projectable.** That is why the
|
|
60
|
+
unsupported helpers below are rejected rather than silently dropped.
|
|
61
|
+
|
|
62
|
+
## Defining and mounting
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
// src/urls/shop.client.tsx
|
|
66
|
+
"use client";
|
|
67
|
+
|
|
68
|
+
import { Suspense } from "react";
|
|
69
|
+
import { clientUrls, useLoader, Link } from "@rangojs/router/client";
|
|
70
|
+
import { ProductsLoader, ProductLoader } from "@/loaders/products.js";
|
|
71
|
+
|
|
72
|
+
function ShopLayout() {
|
|
73
|
+
/* useOutlet().content + chrome */
|
|
74
|
+
}
|
|
75
|
+
function ShopIndex() {
|
|
76
|
+
/* <Suspense><Grid/></Suspense> */
|
|
77
|
+
}
|
|
78
|
+
function ProductPage() {
|
|
79
|
+
/* useLoader(ProductLoader) under Suspense */
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export default clientUrls(({ path, layout, loader, revalidate }) => [
|
|
83
|
+
layout(ShopLayout, () => [
|
|
84
|
+
path("/", ShopIndex, { name: "index" }, () => [loader(ProductsLoader)]),
|
|
85
|
+
path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
86
|
+
loader(ProductLoader, () => [
|
|
87
|
+
revalidate(
|
|
88
|
+
({
|
|
89
|
+
isAction,
|
|
90
|
+
currentParams,
|
|
91
|
+
nextParams,
|
|
92
|
+
defaultShouldRevalidate,
|
|
93
|
+
}) => {
|
|
94
|
+
if (isAction) return false;
|
|
95
|
+
return currentParams.slug !== nextParams.slug
|
|
96
|
+
? defaultShouldRevalidate
|
|
97
|
+
: false;
|
|
98
|
+
},
|
|
99
|
+
),
|
|
100
|
+
]),
|
|
101
|
+
]),
|
|
102
|
+
]),
|
|
103
|
+
]);
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Mount it from the server tree like any module:
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
// src/urls/index.tsx (server)
|
|
110
|
+
import shopUrls from "./shop.client.js";
|
|
111
|
+
|
|
112
|
+
include("/shop", shopUrls, { name: "shop" });
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Route names compose through the include (`shop.index`, `shop.product`) and
|
|
116
|
+
flow into the generated route map, so `href`/`reverse` and `Handler<"...">`
|
|
117
|
+
typing work exactly as for server routes (`/typesafety`).
|
|
118
|
+
|
|
119
|
+
## Helpers: what exists inside clientUrls()
|
|
120
|
+
|
|
121
|
+
| Helper | Notes |
|
|
122
|
+
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
123
|
+
| `path()` | Options are `name`, `search`, `trailingSlash`, `ppr` (shell caching — see /ppr skill; loader routes need `loading()` or capture refuses); no response variants |
|
|
124
|
+
| `layout()` | Must contain at least one `path()` |
|
|
125
|
+
| `loader()` | `loader(Def, use?)` or `loader(Def, { stream: "navigation" }, use?)` — see below |
|
|
126
|
+
| `loading()` | Route/layout-level pending UI; inline `<Suspense>` at read sites is usually better |
|
|
127
|
+
| `revalidate()` | Valid **inside a loader() use callback only**; runs in the browser |
|
|
128
|
+
| `transition()` | Data-only ViewTransition config — no `when` (that is a server-executed predicate) |
|
|
129
|
+
| `intercept()` | Dot-local named target in the SAME definition; use may contain `loader()`/`loading()` |
|
|
130
|
+
|
|
131
|
+
`include`, `parallel`, `cache`, `middleware`, `errorBoundary`,
|
|
132
|
+
`notFoundBoundary` **throw** — and that is a design position, not a gap.
|
|
133
|
+
Client route groups are a PERFORMANCE surface (instant presentation, held
|
|
134
|
+
data, streaming reads), not full-feature routing; the server tree around the
|
|
135
|
+
mount is the full-feature router. Put those helpers at or around the
|
|
136
|
+
`include()` mount in the server `urls()` tree: middleware wrapping the mount
|
|
137
|
+
runs for every route in the group; a `notFoundBoundary()` on an enclosing
|
|
138
|
+
server layout catches loader-thrown `notFound()` from the group. (Caching
|
|
139
|
+
still reaches the group's data the right way: `"use cache"` inside a loader
|
|
140
|
+
body — the loaders stay the live layer, the expensive function is what
|
|
141
|
+
caches.)
|
|
142
|
+
|
|
143
|
+
Boundaries are a deliberate exclusion, not a projection limitation: the
|
|
144
|
+
server-tree boundary around the mount gives group-scoped custom UI with the
|
|
145
|
+
uniform server-resolved envelope (PE/no-JS correct), and INSIDE the group
|
|
146
|
+
plain React error boundaries just work — every component is a client
|
|
147
|
+
component, and a streamed loader rejection throws to the boundary above its
|
|
148
|
+
`useLoader` read (siblings keep reading it from `error`). Per-read boundary +
|
|
149
|
+
inline Suspense is the clientUrls idiom; a route-level DSL boundary would be
|
|
150
|
+
the same regression `loading()` is relative to inline Suspense.
|
|
151
|
+
|
|
152
|
+
## Outer layouts and middleware across group navigations
|
|
153
|
+
|
|
154
|
+
With the common shape — `layout(<AdminRscLayout />, () => [middleware(requireAdmin), include("/admin", adminUrls)])` — middleware and layout handlers ride
|
|
155
|
+
DIFFERENT schedules:
|
|
156
|
+
|
|
157
|
+
- **Middleware runs on EVERY canonical request**, not just the first
|
|
158
|
+
encounter. Every navigation inside the group — including a tab switch whose
|
|
159
|
+
loaders all hold — still sends the canonical partial request (it commits
|
|
160
|
+
URL/history and carries the revalidation decisions), and middleware wraps
|
|
161
|
+
each one: `requireAdmin` re-authorizes every navigation, and its
|
|
162
|
+
`ctx.set()` variables are fresh for the group's loaders on every pass. The
|
|
163
|
+
instant feel comes from held data and optimistic presentation, not from
|
|
164
|
+
skipping the server.
|
|
165
|
+
- **The outer layout handler does NOT re-run on within-group navigations** —
|
|
166
|
+
partial rendering holds its segment (and the parent chain skips on actions
|
|
167
|
+
by default). Layout-rendered data that must track the group belongs in
|
|
168
|
+
middleware `ctx.set()`, not the layout body.
|
|
169
|
+
- The only thing that precedes middleware is the optimistic branch
|
|
170
|
+
(destination `loading()` / `useOutlet().pending`) — presentation, never
|
|
171
|
+
authorization.
|
|
172
|
+
|
|
173
|
+
All three pinned by dev + production e2e in the router repository (a
|
|
174
|
+
middleware-stamped header per request, a layout run-count stamp held across
|
|
175
|
+
navigations).
|
|
176
|
+
|
|
177
|
+
## Revalidation runs in the browser
|
|
178
|
+
|
|
179
|
+
A server-tree `revalidate()` predicate executes on the server. A `clientUrls()`
|
|
180
|
+
predicate is client-module code, so it executes **in the browser** with
|
|
181
|
+
client-computable args — `currentUrl`, `nextUrl`, `currentParams`,
|
|
182
|
+
`nextParams`, `defaultShouldRevalidate`, `stale`, `isAction`, `actionId` — and
|
|
183
|
+
only its _decision_ crosses the wire with the navigation request. Requests that
|
|
184
|
+
carry no decisions (no-JS, progressive enhancement, prefetch, document loads)
|
|
185
|
+
follow the locked server defaults.
|
|
186
|
+
|
|
187
|
+
Two scars worth copying:
|
|
188
|
+
|
|
189
|
+
- A blunt `() => false` keeps serving the OLD product on product→product
|
|
190
|
+
navigations (same route, new param). Make predicates param-sensitive:
|
|
191
|
+
return `defaultShouldRevalidate` when the identifying param changed.
|
|
192
|
+
- One action, per-loader outcomes: a cart badge loader revalidates on actions
|
|
193
|
+
(`isAction ? defaultShouldRevalidate : false`) while product/related loaders
|
|
194
|
+
hold — three freshness outcomes in a single commit, decided per loader.
|
|
195
|
+
|
|
196
|
+
## Loaders are full citizens: signals and handles
|
|
197
|
+
|
|
198
|
+
Everything a DSL loader can do in the server tree works here — the loader body
|
|
199
|
+
is server code either way.
|
|
200
|
+
|
|
201
|
+
**Authority signals.** A loader may `throw notFound()` or
|
|
202
|
+
`throw redirect(url)`:
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
export const ProductLoader = createLoader(async (ctx) => {
|
|
206
|
+
"use server";
|
|
207
|
+
const moved = LEGACY_SLUGS[ctx.params.slug];
|
|
208
|
+
if (moved) throw redirect(`/shop/product/${moved}`);
|
|
209
|
+
if (!(await exists(ctx.params.slug))) notFound(`No "${ctx.params.slug}"`);
|
|
210
|
+
return getProduct(ctx.params.slug);
|
|
211
|
+
});
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
On a document load, `notFound()` streams the resolved not-found UI in the
|
|
215
|
+
envelope and _opportunistically_ sets a real 404 status — the status write only
|
|
216
|
+
wins if the rejection settles before the document Response is constructed (a
|
|
217
|
+
fast, pre-fetch existence check usually wins; see `stream: "navigation"` below
|
|
218
|
+
for the deterministic version). On navigations the 404 UI swaps in with the
|
|
219
|
+
URL preserved. A loader `redirect()` is always a client-side navigate — there
|
|
220
|
+
is no document-lane 302 from loaders; pre-stream redirect authority belongs to
|
|
221
|
+
middleware.
|
|
222
|
+
|
|
223
|
+
**Handle writes.** `ctx.use(Handle)` from the loader body is the WRITE, with
|
|
224
|
+
handler parity — `ctx.use(Meta)({ title })` pushes exactly like a handler
|
|
225
|
+
push. Reads moved to `ctx.get(handle)`, gated behind `await ctx.rendered()`.
|
|
226
|
+
Delivery follows the race model: pushes that settle before the handler barrier
|
|
227
|
+
ride the SSR handle snapshot; later ones stream and apply post-hydration
|
|
228
|
+
(document loads) or progressively (navigations). See `/loader` for the full
|
|
229
|
+
contract.
|
|
230
|
+
|
|
231
|
+
## `stream: "navigation"` — the SSR-completeness opt-in
|
|
232
|
+
|
|
233
|
+
By default every loader streams on every render, so nothing a slow loader
|
|
234
|
+
produces is _guaranteed_ to be in the SSR'd HTML — data SSRs as the Suspense
|
|
235
|
+
fallback, a late handle push applies post-hydration, a late `notFound()` loses
|
|
236
|
+
the status race. When the loader feeds something that must exist in the
|
|
237
|
+
document — `<head>` meta via a handle, or a real 404 status — flag it:
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
241
|
+
loader(ProductLoader, { stream: "navigation" }, () => [revalidate(productData)]),
|
|
242
|
+
loader(RelatedLoader), // untouched: still streams behind its boundary
|
|
243
|
+
]),
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
The name says WHERE streaming still applies, not that it is disabled: document
|
|
247
|
+
renders await this loader before first flush (data settled — no fallback
|
|
248
|
+
paints; handle pushes beat the barrier snapshot; a thrown `notFound()` is a
|
|
249
|
+
deterministic real 404, no warm-up race); client navigations stream exactly as
|
|
250
|
+
before. Scoped per loader — the flagged loader awaits only itself, siblings
|
|
251
|
+
keep streaming.
|
|
252
|
+
|
|
253
|
+
Constraints:
|
|
254
|
+
|
|
255
|
+
- A flagged loader must not `await ctx.rendered()` (or read handles via
|
|
256
|
+
`ctx.get(handle)`, which rendered() gates) — the document render awaits the
|
|
257
|
+
loader _before_ the render barrier resolves, so that wait is a cycle by
|
|
258
|
+
construction. It throws a deadlock error naming the fix.
|
|
259
|
+
- `intercept()` loaders reject the flag — intercepts render on client
|
|
260
|
+
navigations only, so a document-render await can never apply.
|
|
261
|
+
- It does not change PPR capture behavior: capture renders mask loaders and
|
|
262
|
+
skip the await.
|
|
263
|
+
|
|
264
|
+
Every document load pays the flagged loader's latency before first byte —
|
|
265
|
+
that is the point, but it is a real cost. Keep flagged loaders fast (existence
|
|
266
|
+
checks before expensive fetches) and flag the loaders that need it, not the
|
|
267
|
+
route.
|
|
268
|
+
|
|
269
|
+
Pinned end to end by dev + production e2e in the router repository.
|
|
270
|
+
|
|
271
|
+
## Intercepts
|
|
272
|
+
|
|
273
|
+
```tsx
|
|
274
|
+
export default clientUrls(({ path, intercept, loader, loading }) => [
|
|
275
|
+
path("/items", ItemsIndex, { name: "index" }),
|
|
276
|
+
path("/items/:id", ItemDetail, { name: "detail" }),
|
|
277
|
+
intercept("@modal", ".detail", ItemModal, () => [
|
|
278
|
+
loader(ItemLoader),
|
|
279
|
+
loading(<ModalSkeleton />),
|
|
280
|
+
]),
|
|
281
|
+
]);
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Compared to the server `intercept()` (`/intercept`): the target must be a
|
|
285
|
+
dot-local NAMED route in the same definition, there is no `when` selector and
|
|
286
|
+
no middleware, and the use callback accepts `loader()`/`loading()` only —
|
|
287
|
+
every field must be JSON-projectable. Scoping is module-local: only
|
|
288
|
+
navigations originating inside this `clientUrls()` group render the intercept;
|
|
289
|
+
a hard load of the target URL renders the full route.
|
|
290
|
+
|
|
291
|
+
## Pitfalls
|
|
292
|
+
|
|
293
|
+
- **Duplicate patterns or names throw** at definition time, per definition.
|
|
294
|
+
- **`layout()` with no `path()` inside throws** — a client layout exists only
|
|
295
|
+
to wrap routes.
|
|
296
|
+
- **`loading()` vs inline Suspense:** same-route SEARCH navigations
|
|
297
|
+
(filters, tabs) hold previous content by default — the canonical commit
|
|
298
|
+
runs in a `startTransition` when no new segments mount, and the wrapping
|
|
299
|
+
layout sees `useOutlet().pending === true` for the held window; a
|
|
300
|
+
route-level `loading()` no longer re-flashes there. PARAM navigations
|
|
301
|
+
still remount (fresh skeleton, fresh state) unless the route declares
|
|
302
|
+
`transition()` — that opt-in drops the param from the segment key. Inline
|
|
303
|
+
`<Suspense>` above each `useLoader` read is still the finer-grained tool
|
|
304
|
+
when different reads on one route should wait independently.
|
|
305
|
+
- **Two parallel loaders with equal latency look "SSR'd" together.** Loaders
|
|
306
|
+
kick off in parallel, so awaiting one (`stream: "navigation"`) gives
|
|
307
|
+
same-or-faster siblings time to settle coincidentally. Do not read "it was
|
|
308
|
+
in the HTML once" as a guarantee — only the flagged loader is guaranteed.
|
|
309
|
+
- **Hook semantics shift inside a group.** `usePathname` is ABSOLUTE (mount
|
|
310
|
+
included) — never compare it to your own `path()` patterns; build local
|
|
311
|
+
links with `useHref()` (`groupHref("/items/1")` composes the mount).
|
|
312
|
+
`useRouter().push("cart")` (RELATIVE,
|
|
313
|
+
no leading slash) resolves against the mount; `push("/x")` stays
|
|
314
|
+
app-absolute by design (never auto-prefixed). `useSearchParams` carries REAL
|
|
315
|
+
values during SSR (live request seeds the SSR store; on ppr routes the
|
|
316
|
+
capture/resume renders seed the shell key's own sorted search — search is
|
|
317
|
+
part of shell identity, so static-part reads are legal).
|
|
318
|
+
`useNavigation`/`useLinkStatus` report the canonical nav, but a reader
|
|
319
|
+
inside optimistically-swapped content (destination WITH `loading()`)
|
|
320
|
+
unmounts at click — put status readers in surviving chrome. Errors: wrap
|
|
321
|
+
throwers in the client `ErrorBoundary`; `useFetchLoader` works unchanged
|
|
322
|
+
(route-independent lane; `revalidate()` deliberately not consulted).
|
|
323
|
+
Pinned in the router repo's client-urls hook-probe e2e.
|
|
324
|
+
- **Editing the module in dev:** projections refresh via HMR discovery; if a
|
|
325
|
+
route-shape edit ever serves stale routes, restart dev — and if you are
|
|
326
|
+
developing the router itself, rebuild the router dist before `pnpm dev`
|
|
327
|
+
(the discovery machinery ships compiled).
|
|
328
|
+
|
|
329
|
+
## Reference app
|
|
330
|
+
|
|
331
|
+
The router repository (not shipped in this package) carries a canonical
|
|
332
|
+
consumer at `tests/vite-rsc-demo/src/urls/client-shop.client.tsx`: layout +
|
|
333
|
+
index + product routes, param-sensitive predicates, action-scoped cart
|
|
334
|
+
revalidation, loader-thrown `notFound()`/`redirect()`, loader-written
|
|
335
|
+
Meta/Breadcrumbs, viewport prefetch on cards, and the `stream: "navigation"`
|
|
336
|
+
fixtures — with e2e suites next to it pinning each contract in dev and
|
|
337
|
+
production. The sections above are self-contained; the app is corroboration,
|
|
338
|
+
not required reading.
|
|
@@ -601,8 +601,12 @@ the current state for a known no-op.
|
|
|
601
601
|
TanStack Router is the real peer here (`preload="intent|viewport|render"`,
|
|
602
602
|
`preloadDelay`, `preloadStaleTime`); Rango's edge is the resource-aware
|
|
603
603
|
idle + image-ready gating and RSC-payload reuse. Next has an internal prefetch
|
|
604
|
-
scheduler
|
|
605
|
-
|
|
604
|
+
scheduler — the machinery behind its "instant navigations" pitch — but its
|
|
605
|
+
public `<Link prefetch>` surface does not expose Rango's choice of trigger and
|
|
606
|
+
resource gates. Rango's counterpart to instant navigations is the same
|
|
607
|
+
outcome, spelled explicitly: a viewport/hover-warmed Link commits its
|
|
608
|
+
prefetched payload as a whole on click, so the complete page lands with no
|
|
609
|
+
fetch waterfall. Waku has manual route prefetching plus experimental
|
|
606
610
|
`unstable_prefetchOnEnter` / `unstable_prefetchOnView` Link triggers, but without
|
|
607
611
|
this resource-aware policy layer.
|
|
608
612
|
|
|
@@ -613,10 +617,14 @@ excluded from an enclosing segment cache and resolve on every request unless the
|
|
|
613
617
|
loader itself explicitly opts into `cache()`. They may safely read `cookies()`,
|
|
614
618
|
`headers()`, request context, and `env` because loader execution is outside the
|
|
615
619
|
cached shell. Loaders run in parallel, stream independently under `loading()`
|
|
616
|
-
boundaries, compose server-side via `ctx.use(OtherLoader)`, and
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
+
boundaries, compose server-side via `ctx.use(OtherLoader)`, and carry
|
|
621
|
+
route-level authority: they can throw `notFound()`/`redirect()`, WRITE handles
|
|
622
|
+
(`ctx.use(Meta)({ title })` — data-derived page metadata pushed from the data's
|
|
623
|
+
producer), read handle data after the render settles (`ctx.get(handle)` behind
|
|
624
|
+
`await ctx.rendered()`), and opt into `loader(Def, { stream: "navigation" })`
|
|
625
|
+
so a document render awaits them before first flush (deterministic SSR'd data,
|
|
626
|
+
meta, and 404 status). Reads happen through `useLoader` in a client component
|
|
627
|
+
(including its SSR pass) or `useFetchLoader` for standalone client fetches. "Fetchable" loaders are callable
|
|
620
628
|
endpoints with their own middleware and GET/POST/PUT/PATCH/DELETE bodies.
|
|
621
629
|
|
|
622
630
|
One sharp edge is worth stating because the distinction matters: a cached handler
|
|
@@ -682,12 +690,18 @@ that no loader-before-render has:
|
|
|
682
690
|
- it is **standalone and composable**, not route-coupled — one `createLoader()`
|
|
683
691
|
read by many segments, composed via `ctx.use`, or exposed as a fetchable endpoint;
|
|
684
692
|
- it **streams as a hole, not a gate** — concurrent, Suspense-resolved under
|
|
685
|
-
`loading()`, so the shell never blocks on it
|
|
693
|
+
`loading()`, so the shell never blocks on it (and gating is a deliberate
|
|
694
|
+
per-loader opt-in — `{ stream: "navigation" }` awaits ONE loader on document
|
|
695
|
+
renders while the rest keep streaming — not the model's default);
|
|
686
696
|
- it is **client-addressable and refreshable** independent of navigation (`key`,
|
|
687
697
|
`refreshGroup`, `useRefreshLoaders`), behaving like a built-in, server-defined
|
|
688
698
|
data cell with zero loader logic in the client bundle;
|
|
689
|
-
- it
|
|
690
|
-
|
|
699
|
+
- it carries **route authority and document side-effects** — thrown
|
|
700
|
+
`notFound()`/`redirect()` and handle writes (page meta, breadcrumbs) travel
|
|
701
|
+
with the data unit itself, not with a route-coupled function beside it;
|
|
702
|
+
- it can **read render output** via `ctx.get(handle)` behind
|
|
703
|
+
`await ctx.rendered()` — a loader that depends on what the render produced,
|
|
704
|
+
which a strictly before-render model cannot express.
|
|
691
705
|
|
|
692
706
|
The fair objections, and why they do not collapse the distinction:
|
|
693
707
|
|
package/skills/hooks/SKILL.md
CHANGED
|
@@ -28,7 +28,7 @@ the one for your case.
|
|
|
28
28
|
| Stable router actions (push/replace/refresh/…) | `useRouter()` | [`./navigation.md`](./navigation.md) |
|
|
29
29
|
| Current URL path & matched segments | `useSegments()` | [`./navigation.md`](./navigation.md) |
|
|
30
30
|
| Pending state inside a `<Link>` | `useLinkStatus()` | [`./navigation.md`](./navigation.md) |
|
|
31
|
-
| Loader data (
|
|
31
|
+
| Loader data (streams; suspends until it lands) | `useLoader()` | [`./data.md`](./data.md) |
|
|
32
32
|
| Loader data with on-demand fetch | `useFetchLoader()` | [`./data.md`](./data.md) |
|
|
33
33
|
| Refresh multiple loaders across groups | `useRefreshLoaders()` | [`./data.md`](./data.md) |
|
|
34
34
|
| Accumulated handle data from route segments | `useHandle()` | [`./handle-and-actions.md`](./handle-and-actions.md) |
|
|
@@ -69,7 +69,7 @@ the one for your case.
|
|
|
69
69
|
| ------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------ |
|
|
70
70
|
| `useParams()` | Route params | `Readonly<T>` (default `Record<string, string>`) or selected value |
|
|
71
71
|
| `usePathname()` | Current pathname | `string` |
|
|
72
|
-
| `useSearchParams()` | URL search params
|
|
72
|
+
| `useSearchParams()` | URL search params (read + write) | `[ReadonlyURLSearchParams, SetSearchParams]` (RR-style tuple) |
|
|
73
73
|
| `useHref()` | Mount-aware href | `(path) => string` |
|
|
74
74
|
| `useMount()` | Current include() mount path | `string` |
|
|
75
75
|
| `useReverse()` | Local reverse for imported routes | `(name, params?, search?) => string` |
|
package/skills/hooks/data.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
### useLoader()
|
|
4
4
|
|
|
5
|
-
Access loader data (strict
|
|
5
|
+
Access loader data (strict — data guaranteed once the read returns):
|
|
6
6
|
|
|
7
7
|
```tsx
|
|
8
8
|
"use client";
|
|
@@ -13,13 +13,22 @@ function ProductPrice() {
|
|
|
13
13
|
const { data, isLoading, error } = useLoader(ProductLoader);
|
|
14
14
|
|
|
15
15
|
// data: T (guaranteed - throws if not in context)
|
|
16
|
-
// isLoading: boolean
|
|
16
|
+
// isLoading: boolean (refetch/load() states — NOT the initial streamed read)
|
|
17
17
|
// error: Error | null
|
|
18
18
|
|
|
19
19
|
return <span>${data.price}</span>;
|
|
20
20
|
}
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
+
**Loaders stream, and `useLoader` implicitly suspends.** A first read whose
|
|
24
|
+
loader data has not streamed in yet suspends to the nearest `<Suspense>`
|
|
25
|
+
boundary (or the route's `loading()`) — it does NOT render with
|
|
26
|
+
`isLoading: true`. Put a boundary above every read whose loader can be slow;
|
|
27
|
+
`isLoading` covers later refetches (`load()`, key/group refreshes). Once the
|
|
28
|
+
component renders, `data` is present. (On document loads a loader registered
|
|
29
|
+
with `{ stream: "navigation" }` is already settled at first paint, so its
|
|
30
|
+
reads never suspend there — see `/loader`.)
|
|
31
|
+
|
|
23
32
|
**Precondition**: Loader must be registered on route via `loader()` helper.
|
|
24
33
|
|
|
25
34
|
Loaders can also be passed as props from server to client components:
|
|
@@ -30,6 +30,13 @@ function BreadcrumbNav() {
|
|
|
30
30
|
const lastCrumb = useHandle(Breadcrumbs, (data) => data.at(-1));
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
Writers: handlers AND loader bodies — `ctx.use(Handle)` returns the push
|
|
34
|
+
function in each (middleware has no handle APIs). Loader pushes follow a delivery race (settle before
|
|
35
|
+
the handler barrier → in the SSR'd document; later → applied client-side), and
|
|
36
|
+
`loader(Def, { stream: "navigation" })` makes them deterministic in the
|
|
37
|
+
document. See `/loader` → "Writing Handles from Loaders". `useHandle`
|
|
38
|
+
re-renders on every handle update, late loader pushes included.
|
|
39
|
+
|
|
33
40
|
Handles can be passed as props from server to client components:
|
|
34
41
|
|
|
35
42
|
```tsx
|
package/skills/hooks/outlets.md
CHANGED
|
@@ -22,20 +22,41 @@ function DashboardLayout({ children }: { children?: React.ReactNode }) {
|
|
|
22
22
|
|
|
23
23
|
### useOutlet()
|
|
24
24
|
|
|
25
|
-
Access outlet content programmatically:
|
|
25
|
+
Access outlet content and its client-route presentation state programmatically:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
interface OutletState {
|
|
29
|
+
readonly content: ReactNode;
|
|
30
|
+
readonly pending: boolean;
|
|
31
|
+
}
|
|
32
|
+
```
|
|
26
33
|
|
|
27
34
|
```tsx
|
|
28
35
|
"use client";
|
|
29
36
|
import { useOutlet } from "@rangojs/router/client";
|
|
30
37
|
|
|
31
38
|
function ConditionalLayout() {
|
|
32
|
-
const
|
|
33
|
-
// ReactNode | null
|
|
39
|
+
const { content, pending } = useOutlet();
|
|
34
40
|
|
|
35
|
-
return
|
|
36
|
-
<div className="with-content"
|
|
41
|
+
return content ? (
|
|
42
|
+
<div className="with-content" aria-busy={pending}>
|
|
43
|
+
{content}
|
|
44
|
+
</div>
|
|
37
45
|
) : (
|
|
38
46
|
<div className="empty">No content</div>
|
|
39
47
|
);
|
|
40
48
|
}
|
|
41
49
|
```
|
|
50
|
+
|
|
51
|
+
Migration from the old bare-node return is `const { content, pending } =
|
|
52
|
+
useOutlet()`, then render `content` where you previously rendered the hook result.
|
|
53
|
+
`<Outlet />` itself is unchanged.
|
|
54
|
+
|
|
55
|
+
`pending` is narrow. After hydration, a `clientUrls()` layout receives `true`
|
|
56
|
+
while a browser-local match to a different client route beneath it is presenting
|
|
57
|
+
optimistic loading or retaining the current branch until canonical partial Flight
|
|
58
|
+
settles. It clears on commit, error, redirect, cancellation, or supersession. It
|
|
59
|
+
is `false` during SSR and does not describe ordinary server-route navigation,
|
|
60
|
+
prefetch, generic Suspense, unrelated actions, or a params/search change that
|
|
61
|
+
keeps the same client route record. Use `useNavigation()`, `useLinkStatus()`, or
|
|
62
|
+
loader state for those scopes.
|