@rangojs/router 0.0.0-experimental.e9c0b2f2 → 0.0.0-experimental.ea9f40f2
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/AGENTS.md +6 -10
- package/README.md +289 -938
- package/dist/bin/rango.js +271 -46
- package/dist/vite/index.js +673 -193
- package/package.json +10 -8
- package/skills/api-client/SKILL.md +1 -1
- package/skills/breadcrumbs/SKILL.md +31 -14
- package/skills/cache-guide/SKILL.md +5 -2
- package/skills/caching/SKILL.md +59 -4
- package/skills/catalog.json +271 -0
- package/skills/comparison/SKILL.md +50 -0
- package/skills/comparison/agents/openai.yaml +4 -0
- package/skills/comparison/references/framework-comparison.md +837 -0
- package/skills/composability/SKILL.md +83 -2
- package/skills/debug-manifest/SKILL.md +1 -1
- package/skills/defer-hydration/SKILL.md +235 -0
- package/skills/document-cache/SKILL.md +9 -1
- package/skills/fonts/SKILL.md +1 -1
- package/skills/handler-use/SKILL.md +8 -8
- package/skills/hooks/SKILL.md +54 -892
- package/skills/hooks/data.md +273 -0
- package/skills/hooks/handle-and-actions.md +103 -0
- package/skills/hooks/navigation.md +110 -0
- package/skills/hooks/outlets.md +41 -0
- package/skills/hooks/state.md +228 -0
- package/skills/hooks/urls.md +135 -0
- package/skills/host-router/SKILL.md +4 -4
- package/skills/i18n/SKILL.md +1 -1
- package/skills/intercept/SKILL.md +46 -14
- package/skills/layout/SKILL.md +27 -10
- package/skills/links/SKILL.md +1 -1
- package/skills/loader/SKILL.md +23 -1
- package/skills/middleware/SKILL.md +7 -3
- package/skills/migrate-nextjs/SKILL.md +167 -6
- package/skills/migrate-react-router/SKILL.md +59 -677
- package/skills/migrate-react-router/cloudflare-workers.md +129 -0
- package/skills/migrate-react-router/component-migration.md +196 -0
- package/skills/migrate-react-router/data-and-actions.md +225 -0
- package/skills/migrate-react-router/route-mapping.md +271 -0
- package/skills/mime-routes/SKILL.md +1 -1
- package/skills/observability/SKILL.md +9 -1
- package/skills/parallel/SKILL.md +23 -4
- package/skills/ppr/SKILL.md +622 -0
- package/skills/prerender/SKILL.md +28 -18
- package/skills/rango/SKILL.md +84 -25
- package/skills/response-routes/SKILL.md +15 -1
- package/skills/route/SKILL.md +71 -4
- package/skills/router-setup/SKILL.md +14 -3
- package/skills/scripts/SKILL.md +1 -1
- package/skills/server-actions/SKILL.md +3 -2
- package/skills/shell-manifest/SKILL.md +185 -0
- package/skills/streams-and-websockets/SKILL.md +1 -1
- package/skills/tailwind/SKILL.md +1 -1
- package/skills/testing/SKILL.md +2 -1
- package/skills/testing/handles.md +4 -2
- package/skills/testing/render-handler.md +15 -14
- package/skills/testing/reverse-and-types.md +8 -7
- package/skills/theme/SKILL.md +1 -1
- package/skills/typesafety/SKILL.md +45 -919
- package/skills/typesafety/env-and-bindings.md +254 -0
- package/skills/typesafety/generated-files-and-cli.md +335 -0
- package/skills/typesafety/params-and-search.md +153 -0
- package/skills/typesafety/route-types.md +209 -0
- package/skills/use-cache/SKILL.md +30 -3
- package/skills/vercel/SKILL.md +1 -1
- package/skills/view-transitions/SKILL.md +44 -1
- package/src/browser/event-controller.ts +62 -10
- package/src/browser/logging.ts +28 -0
- package/src/browser/merge-segment-loaders.ts +6 -4
- package/src/browser/navigation-bridge.ts +65 -16
- package/src/browser/navigation-client.ts +32 -2
- package/src/browser/navigation-store.ts +128 -14
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +76 -17
- package/src/browser/prefetch/cache.ts +51 -11
- package/src/browser/prefetch/fetch.ts +59 -21
- package/src/browser/prefetch/queue.ts +19 -4
- package/src/browser/react/Link.tsx +13 -3
- package/src/browser/react/NavigationProvider.tsx +108 -4
- package/src/browser/response-adapter.ts +38 -9
- package/src/browser/rsc-router.tsx +54 -4
- package/src/browser/scroll-restoration.ts +7 -5
- package/src/browser/segment-reconciler.ts +31 -21
- package/src/browser/server-action-bridge.ts +22 -10
- package/src/browser/types.ts +54 -1
- package/src/build/generate-manifest.ts +155 -131
- package/src/build/index.ts +3 -1
- package/src/build/route-trie.ts +35 -7
- package/src/build/route-types/include-resolution.ts +347 -47
- package/src/build/runtime-discovery.ts +4 -1
- package/src/cache/cache-key-utils.ts +29 -0
- package/src/cache/cache-runtime.ts +262 -71
- package/src/cache/cache-scope.ts +2 -17
- package/src/cache/cache-tag.ts +60 -14
- package/src/cache/cf/cf-cache-store.ts +243 -20
- package/src/cache/document-cache.ts +54 -21
- package/src/cache/index.ts +1 -0
- package/src/cache/memory-segment-store.ts +110 -3
- package/src/cache/profile-registry.ts +15 -0
- package/src/cache/read-through-swr.ts +15 -1
- package/src/cache/segment-codec.ts +4 -4
- package/src/cache/shell-snapshot.ts +417 -0
- package/src/cache/types.ts +158 -0
- package/src/cache/vercel/vercel-cache-store.ts +401 -124
- package/src/client.rsc.tsx +0 -3
- package/src/client.tsx +0 -3
- package/src/cloudflare/tracing.ts +7 -8
- package/src/defer.ts +11 -22
- package/src/handle.ts +37 -15
- package/src/handles/MetaTags.tsx +16 -82
- package/src/handles/breadcrumbs.ts +12 -14
- package/src/handles/deferred-resolution.ts +127 -0
- package/src/handles/is-thenable.ts +7 -8
- package/src/handles/meta.ts +7 -44
- package/src/host/errors.ts +15 -0
- package/src/host/index.ts +1 -0
- package/src/index.rsc.ts +8 -2
- package/src/index.ts +19 -13
- package/src/internal-debug.ts +11 -8
- package/src/prerender.ts +17 -4
- package/src/redirect-origin.ts +14 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/route-content-wrapper.tsx +12 -5
- package/src/route-definition/dsl-helpers.ts +21 -32
- package/src/route-definition/helper-factories.ts +0 -2
- package/src/route-definition/helpers-types.ts +43 -43
- package/src/route-definition/index.ts +1 -2
- package/src/route-definition/resolve-handler-use.ts +0 -1
- package/src/route-definition/use-item-types.ts +3 -6
- package/src/route-map-builder.ts +41 -4
- package/src/route-types.ts +0 -5
- package/src/router/find-match.ts +86 -8
- package/src/router/instrument.ts +9 -4
- package/src/router/lazy-includes.ts +72 -12
- package/src/router/loader-resolution.ts +14 -2
- package/src/router/manifest.ts +56 -11
- package/src/router/match-api.ts +76 -32
- package/src/router/match-handlers.ts +181 -135
- package/src/router/match-middleware/background-revalidation.ts +40 -23
- package/src/router/match-middleware/cache-store.ts +39 -24
- package/src/router/match-result.ts +35 -15
- package/src/router/middleware.ts +64 -38
- package/src/router/navigation-snapshot.ts +7 -5
- package/src/router/parse-pattern.ts +115 -0
- package/src/router/pattern-matching.ts +53 -64
- package/src/router/prefetch-limits.ts +37 -0
- package/src/router/prerender-match.ts +11 -5
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +23 -8
- package/src/router/route-snapshot.ts +14 -2
- package/src/router/router-context.ts +3 -1
- package/src/router/router-interfaces.ts +32 -1
- package/src/router/router-options.ts +30 -0
- package/src/router/segment-resolution/fresh.ts +39 -3
- package/src/router/segment-resolution/loader-cache.ts +93 -2
- package/src/router/segment-resolution/loader-mask.ts +60 -0
- package/src/router/segment-resolution/loader-snapshot.ts +259 -0
- package/src/router/segment-resolution/mask-nested.ts +83 -0
- package/src/router/segment-resolution/revalidation.ts +3 -0
- package/src/router/segment-resolution/view-transition-default.ts +35 -15
- package/src/router/substitute-pattern-params.ts +54 -35
- package/src/router/telemetry-otel.ts +6 -8
- package/src/router/telemetry.ts +9 -1
- package/src/router/tracing.ts +14 -5
- package/src/router/trie-matching.ts +19 -11
- package/src/router/url-params.ts +13 -0
- package/src/router.ts +47 -16
- package/src/rsc/full-payload.ts +70 -0
- package/src/rsc/handler.ts +60 -33
- package/src/rsc/manifest-init.ts +1 -1
- package/src/rsc/nonce.ts +10 -1
- package/src/rsc/progressive-enhancement.ts +61 -4
- package/src/rsc/redirect-guard.ts +2 -1
- package/src/rsc/rsc-rendering.ts +429 -37
- package/src/rsc/server-action.ts +25 -2
- package/src/rsc/shell-capture.ts +1190 -0
- package/src/rsc/shell-serve.ts +181 -0
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +30 -0
- package/src/segment-loader-promise.ts +18 -0
- package/src/segment-system.tsx +149 -14
- package/src/server/context.ts +67 -9
- package/src/server/cookie-store.ts +73 -1
- package/src/server/loader-registry.ts +13 -1
- package/src/server/request-context.ts +169 -10
- package/src/ssr/index.tsx +462 -178
- package/src/ssr/inject-rsc-eager.ts +167 -0
- package/src/ssr/ssr-root.tsx +228 -0
- package/src/testing/collect-handle.ts +14 -8
- package/src/testing/dispatch.ts +152 -40
- package/src/testing/generated-routes.ts +27 -11
- package/src/testing/index.ts +6 -0
- package/src/testing/render-handler.ts +14 -0
- package/src/testing/render-route.tsx +13 -10
- package/src/testing/run-transition-when.ts +164 -0
- package/src/theme/ThemeProvider.tsx +36 -26
- package/src/types/handler-context.ts +1 -1
- package/src/types/index.ts +2 -0
- package/src/types/route-config.ts +19 -7
- package/src/types/segments.ts +100 -0
- package/src/urls/include-helper.ts +10 -8
- package/src/urls/include-provider.ts +71 -0
- package/src/urls/index.ts +1 -0
- package/src/urls/path-helper-types.ts +44 -12
- package/src/urls/path-helper.ts +5 -0
- package/src/urls/pattern-types.ts +36 -0
- package/src/urls/type-extraction.ts +43 -18
- package/src/urls/urls-function.ts +0 -1
- package/src/vercel/tracing.ts +7 -7
- package/src/vite/discovery/dev-prerender-cache.ts +117 -0
- package/src/vite/discovery/discover-routers.ts +1 -1
- package/src/vite/discovery/discovery-errors.ts +61 -0
- package/src/vite/index.ts +7 -0
- package/src/vite/inject-client-debug.ts +88 -0
- package/src/vite/plugins/vercel-output.ts +114 -25
- package/src/vite/plugins/version-injector.ts +22 -7
- package/src/vite/plugins/virtual-entries.ts +80 -22
- package/src/vite/rango.ts +29 -19
- package/src/vite/router-discovery.ts +171 -43
- package/src/vite/utils/prerender-utils.ts +17 -4
- package/src/vite/utils/shared-utils.ts +47 -0
- package/src/network-error-thrower.tsx +0 -18
package/skills/hooks/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: hooks
|
|
3
|
-
description: Client-side React hooks for navigation, loaders, and state in @rangojs/router
|
|
3
|
+
description: Client-side React hooks for navigation, loaders, and state in @rangojs/router. Use when a client component needs the current URL, params, search params, navigation state, or loader data — e.g. "how do I read the route param in a component".
|
|
4
4
|
argument-hint: [hook-name]
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -9,897 +9,59 @@ argument-hint: [hook-name]
|
|
|
9
9
|
Import the hooks and components in this skill from `@rangojs/router/client`.
|
|
10
10
|
The root `@rangojs/router` entrypoint is for server/RSC APIs and shared types.
|
|
11
11
|
|
|
12
|
-
##
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
```tsx
|
|
66
|
-
// Update search params without server round-trip
|
|
67
|
-
router.push("/products?color=blue", { revalidate: false });
|
|
68
|
-
router.replace("/products?page=3", { revalidate: false });
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
If the pathname changes, `revalidate: false` is silently ignored and a full navigation occurs. This also works on `<Link>`:
|
|
72
|
-
|
|
73
|
-
```tsx
|
|
74
|
-
<Link to="/products?color=blue" revalidate={false}>
|
|
75
|
-
Blue
|
|
76
|
-
</Link>
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
Plain `<a>` tags can opt in via `data-revalidate="false"`.
|
|
80
|
-
|
|
81
|
-
### useSegments()
|
|
82
|
-
|
|
83
|
-
Access current URL path and matched route segments:
|
|
84
|
-
|
|
85
|
-
```tsx
|
|
86
|
-
"use client";
|
|
87
|
-
import { useSegments } from "@rangojs/router/client";
|
|
88
|
-
|
|
89
|
-
function Breadcrumbs() {
|
|
90
|
-
const { path, segmentIds, location } = useSegments();
|
|
91
|
-
|
|
92
|
-
// path: ["shop", "products", "123"] (split on "/", no leading slash on any element)
|
|
93
|
-
// segmentIds: ["L0", "L0L1", "L0L1R0"] (opaque internal short-codes, not route names)
|
|
94
|
-
// location: URL object
|
|
95
|
-
|
|
96
|
-
return <nav>{path.join(" > ")}</nav>;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// With selector
|
|
100
|
-
const isShopRoute = useSegments((s) => s.path[0] === "shop");
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
### useLinkStatus()
|
|
104
|
-
|
|
105
|
-
Track pending state inside a Link component:
|
|
106
|
-
|
|
107
|
-
```tsx
|
|
108
|
-
"use client";
|
|
109
|
-
import { Link, useLinkStatus } from "@rangojs/router/client";
|
|
110
|
-
|
|
111
|
-
function LoadingIndicator() {
|
|
112
|
-
const { pending } = useLinkStatus();
|
|
113
|
-
return pending ? <Spinner /> : null;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Must be inside Link
|
|
117
|
-
<Link to="/dashboard">
|
|
118
|
-
Dashboard
|
|
119
|
-
<LoadingIndicator />
|
|
120
|
-
</Link>;
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
## Data Hooks
|
|
124
|
-
|
|
125
|
-
### useLoader()
|
|
126
|
-
|
|
127
|
-
Access loader data (strict - data guaranteed):
|
|
128
|
-
|
|
129
|
-
```tsx
|
|
130
|
-
"use client";
|
|
131
|
-
import { useLoader } from "@rangojs/router/client";
|
|
132
|
-
import { ProductLoader } from "../loaders/product";
|
|
133
|
-
|
|
134
|
-
function ProductPrice() {
|
|
135
|
-
const { data, isLoading, error } = useLoader(ProductLoader);
|
|
136
|
-
|
|
137
|
-
// data: T (guaranteed - throws if not in context)
|
|
138
|
-
// isLoading: boolean
|
|
139
|
-
// error: Error | null
|
|
140
|
-
|
|
141
|
-
return <span>${data.price}</span>;
|
|
142
|
-
}
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
**Precondition**: Loader must be registered on route via `loader()` helper.
|
|
146
|
-
|
|
147
|
-
Loaders can also be passed as props from server to client components:
|
|
148
|
-
|
|
149
|
-
```tsx
|
|
150
|
-
"use client";
|
|
151
|
-
import { useLoader } from "@rangojs/router/client";
|
|
152
|
-
import type { ProductLoader } from "../loaders";
|
|
153
|
-
|
|
154
|
-
// typeof infers the full data type from the loader definition
|
|
155
|
-
function ProductCard({ loader }: { loader: typeof ProductLoader }) {
|
|
156
|
-
const { data } = useLoader(loader);
|
|
157
|
-
return <h2>{data.product.name}</h2>;
|
|
158
|
-
}
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
### useFetchLoader()
|
|
162
|
-
|
|
163
|
-
Access loader with on-demand fetching (flexible):
|
|
164
|
-
|
|
165
|
-
```tsx
|
|
166
|
-
"use client";
|
|
167
|
-
import { useFetchLoader } from "@rangojs/router/client";
|
|
168
|
-
import { SearchLoader } from "../loaders/search";
|
|
169
|
-
|
|
170
|
-
function SearchResults() {
|
|
171
|
-
const { data, load, isLoading, error } = useFetchLoader(SearchLoader);
|
|
172
|
-
|
|
173
|
-
// data: T | undefined (may be undefined if not fetched)
|
|
174
|
-
// load: (options?) => Promise<T>
|
|
175
|
-
// refetch: alias for load
|
|
176
|
-
|
|
177
|
-
const handleSearch = async (query: string) => {
|
|
178
|
-
await load({ params: { query } });
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
return (
|
|
182
|
-
<div>
|
|
183
|
-
<input onChange={(e) => handleSearch(e.target.value)} />
|
|
184
|
-
{isLoading && <Spinner />}
|
|
185
|
-
{data?.results.map((r) => (
|
|
186
|
-
<Result key={r.id} {...r} />
|
|
187
|
-
))}
|
|
188
|
-
</div>
|
|
189
|
-
);
|
|
190
|
-
}
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
**Shared refetch behavior**:
|
|
194
|
-
|
|
195
|
-
When the loader is registered on the route via `loader()`, a plain
|
|
196
|
-
`load()` call (no options, or a trivially-defaulted GET with no
|
|
197
|
-
`params` and no `body`) broadcasts its result to every component
|
|
198
|
-
reading the same loader id. Layout, page, and parallel-slot reads
|
|
199
|
-
all converge on the new value:
|
|
200
|
-
|
|
201
|
-
```tsx
|
|
202
|
-
// Layout button calls load() — the page read below sees the update too.
|
|
203
|
-
function Layout() {
|
|
204
|
-
const { data, load } = useLoader(CartLoader);
|
|
205
|
-
return <button onClick={() => load()}>Refresh ({data.count})</button>;
|
|
206
|
-
}
|
|
207
|
-
function Page() {
|
|
208
|
-
const { data } = useLoader(CartLoader); // updates with the layout's load()
|
|
209
|
-
return <span>{data.count} items</span>;
|
|
210
|
-
}
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
`isLoading` and `error` follow the same scope. `throwOnError: true`
|
|
214
|
-
render-throws are scoped to the **originating** hook — sibling readers
|
|
215
|
-
see the error in their `error` state but their boundaries are not
|
|
216
|
-
triggered by someone else's failure. A successful follow-up `load()`
|
|
217
|
-
clears the shared error.
|
|
218
|
-
|
|
219
|
-
**`load()` calls that stay local** (no broadcast, per-hook state, same
|
|
220
|
-
semantics as the old per-component `useState`):
|
|
221
|
-
|
|
222
|
-
- `load({ params: { ... } })` — explicit params.
|
|
223
|
-
- `load({ method: "POST", body })` — mutations.
|
|
224
|
-
- Any `load()` on a `useFetchLoader(loader)` whose loader is **not**
|
|
225
|
-
registered on the current route. Two unrelated components calling
|
|
226
|
-
`load()` on the same fetchable-but-unregistered loader keep
|
|
227
|
-
independent results.
|
|
228
|
-
|
|
229
|
-
So the search/list pattern still works — two components calling
|
|
230
|
-
`load({ params: { q } })` with different `q` values each keep their
|
|
231
|
-
own result; they do not collapse to last-write-wins through a shared
|
|
232
|
-
store.
|
|
233
|
-
|
|
234
|
-
**Scoping refetch with a `key`**:
|
|
235
|
-
|
|
236
|
-
Pass a `key` to partition the shared refresh store. Only hooks using the
|
|
237
|
-
**same** `key` refresh together when one of them calls `load()`. This is a
|
|
238
|
-
client-side refresh identity only — it never changes the request sent to the
|
|
239
|
-
server, and is unrelated to the server `cache({ key })` option and to
|
|
240
|
-
`revalidate()`.
|
|
241
|
-
|
|
242
|
-
```tsx
|
|
243
|
-
// Two independent dashboards using the same loader. Without a key, one
|
|
244
|
-
// dashboard's load() would flip the other's spinner and value. With a key,
|
|
245
|
-
// they refresh independently.
|
|
246
|
-
function Dashboard({ id }: { id: string }) {
|
|
247
|
-
const { data, load } = useLoader(StatsLoader, { key: `dashboard:${id}` });
|
|
248
|
-
return <button onClick={() => load()}>Refresh {data.total}</button>;
|
|
249
|
-
}
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
The `key` widens sharing in two ways the default cannot:
|
|
253
|
-
|
|
254
|
-
- **Parameterized GETs share.** `useFetchLoader(SearchLoader, { key: q })`
|
|
255
|
-
with the same `q` in two components share one result and refresh together —
|
|
256
|
-
a keyed `load({ params: { q } })` broadcasts to the group instead of staying
|
|
257
|
-
local. (Mutations — non-GET or `body` — stay local even with a key.)
|
|
258
|
-
- **Unregistered loaders share.** A `key` makes `useFetchLoader` of a loader
|
|
259
|
-
that is **not** registered on the route share too, letting unrelated
|
|
260
|
-
components opt into a common refresh group.
|
|
261
|
-
|
|
262
|
-
Lifecycle: a keyed read of an unregistered loader is reference-counted — its
|
|
263
|
-
shared value lives as long as at least one component using that key is mounted.
|
|
264
|
-
A persistent component (e.g. a header) keeps the value across navigations; a
|
|
265
|
-
route-scoped component's value is reclaimed when it unmounts. Registered-loader
|
|
266
|
-
reads (keyed or not) reset on navigation from fresh route data, as before.
|
|
267
|
-
|
|
268
|
-
**Refreshing multiple loaders together (`refreshGroup` + `useRefreshLoaders`)**:
|
|
269
|
-
|
|
270
|
-
`key` groups readers of one loader. To refresh **different** loaders together,
|
|
271
|
-
tag them with a shared `refreshGroup` name and trigger them with
|
|
272
|
-
`useRefreshLoaders()`. The hook takes no argument; you pass the group(s) to the
|
|
273
|
-
function it returns, so one `useRefreshLoaders()` can refresh different groups
|
|
274
|
-
depending on context. A read may carry **several** tags — pass an array — and is
|
|
275
|
-
refreshed when **any** of its groups is refreshed:
|
|
276
|
-
|
|
277
|
-
```tsx
|
|
278
|
-
function Profile() {
|
|
279
|
-
const { data } = useLoader(ProfileLoader, {
|
|
280
|
-
key: userId,
|
|
281
|
-
refreshGroup: "account",
|
|
282
|
-
});
|
|
283
|
-
return <span>{data.name}</span>;
|
|
284
|
-
}
|
|
285
|
-
function Orders() {
|
|
286
|
-
// Tagged into two groups: refreshed by "account" (the whole set) or the
|
|
287
|
-
// finer "orders" tag.
|
|
288
|
-
const { data } = useLoader(OrdersLoader, {
|
|
289
|
-
key: userId,
|
|
290
|
-
refreshGroup: ["account", "orders"],
|
|
291
|
-
});
|
|
292
|
-
return <span>{data.count} orders</span>;
|
|
293
|
-
}
|
|
294
|
-
function RefreshButtons() {
|
|
295
|
-
const refresh = useRefreshLoaders();
|
|
296
|
-
return (
|
|
297
|
-
<>
|
|
298
|
-
<button onClick={() => refresh("account")}>Refresh account</button>
|
|
299
|
-
<button onClick={() => refresh("orders")}>Refresh orders only</button>
|
|
300
|
-
<button onClick={() => refresh(["account", "orders"])}>
|
|
301
|
-
Refresh both
|
|
302
|
-
</button>
|
|
303
|
-
</>
|
|
304
|
-
);
|
|
305
|
-
}
|
|
306
|
-
```
|
|
307
|
-
|
|
308
|
-
`refresh(groups)` accepts one name or an array and re-runs every currently-mounted
|
|
309
|
-
member tagged with **any** of them, with a **plain GET** against the current route
|
|
310
|
-
URL — no params, no body, no mutation methods, because a group spans loaders with
|
|
311
|
-
different shapes. A member that sits in two of the requested groups is fetched
|
|
312
|
-
once (members are unioned and deduped by read). It returns a promise that resolves
|
|
313
|
-
when all members settle and **rejects with an `AggregateError`** if any fail;
|
|
314
|
-
group refresh never render-throws, so handle failures at the await site
|
|
315
|
-
(`await refresh("account").catch(...)`). Each failing member also exposes its
|
|
316
|
-
error via its own read's `error`.
|
|
317
|
-
|
|
318
|
-
Multiple tags give you granular vs. whole-set refresh from one place: a coarse
|
|
319
|
-
tag (`"account"`) covers everything, while a finer tag (`"orders"`) targets a
|
|
320
|
-
subset. Sharing within a group is opt-in via `key`: members that share a `key`
|
|
321
|
-
share one value (and one fetch); a grouped reader **without** a `key` gets its own
|
|
322
|
-
private bucket, so a group refresh updates only that read and never leaks into
|
|
323
|
-
unrelated unkeyed reads of the same loader. A bucket may belong to several groups
|
|
324
|
-
at once (one read tagged with multiple names, or different reads tagging the same
|
|
325
|
-
keyed bucket with different names). Keep parameterized loaders on the single-loader
|
|
326
|
-
`key` — a plain-GET group refresh sends no params.
|
|
327
|
-
|
|
328
|
-
**Load options**:
|
|
329
|
-
|
|
330
|
-
```tsx
|
|
331
|
-
// JSON body — sent as application/json, available as ctx.body on the server
|
|
332
|
-
await load({
|
|
333
|
-
method: "POST",
|
|
334
|
-
params: { query: "test" },
|
|
335
|
-
body: { data: "value" },
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
// FormData body — sent as multipart/form-data, available as ctx.formData on the server.
|
|
339
|
-
// Automatically detected: when body is a FormData instance, the request switches
|
|
340
|
-
// to multipart/form-data to preserve File objects and binary data.
|
|
341
|
-
const formData = new FormData();
|
|
342
|
-
formData.append("file", fileInput.files[0]);
|
|
343
|
-
await load({ method: "POST", body: formData });
|
|
344
|
-
```
|
|
345
|
-
|
|
346
|
-
**Body type auto-switching**: The `load()` function inspects the `body` value to
|
|
347
|
-
choose the encoding. If `body instanceof FormData`, the request is sent as
|
|
348
|
-
`multipart/form-data` (browser sets the boundary header automatically). Otherwise
|
|
349
|
-
the body is JSON-serialized and sent with `Content-Type: application/json`. On the
|
|
350
|
-
server, JSON bodies are available via `ctx.body` and FormData bodies via `ctx.formData`.
|
|
351
|
-
|
|
352
|
-
**File upload example**:
|
|
353
|
-
|
|
354
|
-
```tsx
|
|
355
|
-
"use client";
|
|
356
|
-
import { useFetchLoader } from "@rangojs/router/client";
|
|
357
|
-
import { FileUploadLoader } from "../loaders/upload";
|
|
358
|
-
|
|
359
|
-
function FileUploader() {
|
|
360
|
-
const { data, load, isLoading } = useFetchLoader(FileUploadLoader);
|
|
361
|
-
const formRef = useRef<HTMLFormElement>(null);
|
|
362
|
-
|
|
363
|
-
const handleSubmit = async (formData: FormData) => {
|
|
364
|
-
await load({ method: "POST", body: formData });
|
|
365
|
-
formRef.current?.reset();
|
|
366
|
-
};
|
|
367
|
-
|
|
368
|
-
return (
|
|
369
|
-
<form ref={formRef} action={handleSubmit}>
|
|
370
|
-
<input type="file" name="file" />
|
|
371
|
-
<button type="submit" disabled={isLoading}>
|
|
372
|
-
{isLoading ? "Uploading..." : "Upload"}
|
|
373
|
-
</button>
|
|
374
|
-
{data?.uploadedFile && <p>Uploaded: {data.uploadedFile.name}</p>}
|
|
375
|
-
</form>
|
|
376
|
-
);
|
|
377
|
-
}
|
|
378
|
-
```
|
|
379
|
-
|
|
380
|
-
Server-side loader for the upload:
|
|
381
|
-
|
|
382
|
-
```typescript
|
|
383
|
-
import { createLoader } from "@rangojs/router";
|
|
384
|
-
|
|
385
|
-
export const FileUploadLoader = createLoader(async (ctx) => {
|
|
386
|
-
"use server";
|
|
387
|
-
|
|
388
|
-
const file = ctx.formData?.get("file") as File | null;
|
|
389
|
-
if (file && file.size > 0) {
|
|
390
|
-
// Process file (save to R2, D1, etc.)
|
|
391
|
-
return { uploadedFile: { name: file.name, size: file.size } };
|
|
392
|
-
}
|
|
393
|
-
return { uploadedFile: null };
|
|
394
|
-
}, true); // true = fetchable (can be called from the client via load())
|
|
395
|
-
```
|
|
396
|
-
|
|
397
|
-
## Handle Hooks
|
|
398
|
-
|
|
399
|
-
### useHandle()
|
|
400
|
-
|
|
401
|
-
Access accumulated handle data from route segments:
|
|
402
|
-
|
|
403
|
-
```tsx
|
|
404
|
-
"use client";
|
|
405
|
-
import { useHandle, Breadcrumbs } from "@rangojs/router/client";
|
|
406
|
-
|
|
407
|
-
function BreadcrumbNav() {
|
|
408
|
-
const crumbs = useHandle(Breadcrumbs);
|
|
409
|
-
// Array of { label, href } accumulated from layouts/routes
|
|
410
|
-
|
|
411
|
-
return (
|
|
412
|
-
<nav>
|
|
413
|
-
{crumbs.map((c, i) => (
|
|
414
|
-
<span key={i}>
|
|
415
|
-
<a href={c.href}>{c.label}</a>
|
|
416
|
-
{i < crumbs.length - 1 && " > "}
|
|
417
|
-
</span>
|
|
418
|
-
))}
|
|
419
|
-
</nav>
|
|
420
|
-
);
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// With selector
|
|
424
|
-
const lastCrumb = useHandle(Breadcrumbs, (data) => data.at(-1));
|
|
425
|
-
```
|
|
426
|
-
|
|
427
|
-
Handles can be passed as props from server to client components:
|
|
428
|
-
|
|
429
|
-
```tsx
|
|
430
|
-
// Server component
|
|
431
|
-
path("/dashboard", (ctx) => {
|
|
432
|
-
const push = ctx.use(Breadcrumbs);
|
|
433
|
-
push({ label: "Dashboard", href: "/dashboard" });
|
|
434
|
-
return <DashboardNav handle={Breadcrumbs} />;
|
|
435
|
-
});
|
|
436
|
-
```
|
|
437
|
-
|
|
438
|
-
```tsx
|
|
439
|
-
// Client component — typeof infers the full Handle<T> type
|
|
440
|
-
"use client";
|
|
441
|
-
import { useHandle, type Breadcrumbs } from "@rangojs/router/client";
|
|
442
|
-
|
|
443
|
-
function DashboardNav({ handle }: { handle: typeof Breadcrumbs }) {
|
|
444
|
-
const crumbs = useHandle(handle);
|
|
445
|
-
return (
|
|
446
|
-
<nav>
|
|
447
|
-
{crumbs.map((c) => (
|
|
448
|
-
<a href={c.href}>{c.label}</a>
|
|
449
|
-
))}
|
|
450
|
-
</nav>
|
|
451
|
-
);
|
|
452
|
-
}
|
|
453
|
-
```
|
|
454
|
-
|
|
455
|
-
RSC serialization strips the `collect` function via `toJSON()`. On the client,
|
|
456
|
-
`useHandle()` recovers it from the module-level registry (populated when
|
|
457
|
-
`createHandle()` runs during module initialization).
|
|
458
|
-
|
|
459
|
-
## Action Hooks
|
|
460
|
-
|
|
461
|
-
For the full server-action guide (defining actions, `useActionState`,
|
|
462
|
-
`useOptimistic`, validation, revalidation, error handling, file uploads),
|
|
463
|
-
see `/server-actions`. `useAction()` below is a Rango-specific hook for
|
|
464
|
-
tracking actions called outside a `<form action={...}>` flow.
|
|
465
|
-
|
|
466
|
-
### useAction()
|
|
467
|
-
|
|
468
|
-
Track state of server action invocations:
|
|
469
|
-
|
|
470
|
-
```tsx
|
|
471
|
-
"use client";
|
|
472
|
-
import { useAction } from "@rangojs/router/client";
|
|
473
|
-
import { addToCart } from "../actions/cart";
|
|
474
|
-
|
|
475
|
-
function AddToCartButton({ productId }: { productId: string }) {
|
|
476
|
-
const { state, error, result } = useAction(addToCart);
|
|
477
|
-
|
|
478
|
-
// state: 'idle' | 'loading' | 'streaming'
|
|
479
|
-
// actionId: string | null
|
|
480
|
-
// payload: unknown | null (input data)
|
|
481
|
-
// error: Error | null
|
|
482
|
-
// result: unknown | null (return value)
|
|
483
|
-
|
|
484
|
-
return (
|
|
485
|
-
<form action={addToCart}>
|
|
486
|
-
<input type="hidden" name="productId" value={productId} />
|
|
487
|
-
<button disabled={state === "loading"}>
|
|
488
|
-
{state === "loading" ? "Adding..." : "Add to Cart"}
|
|
489
|
-
</button>
|
|
490
|
-
{error && <p className="error">{error.message}</p>}
|
|
491
|
-
</form>
|
|
492
|
-
);
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
// Match by string suffix (convenient but may be ambiguous)
|
|
496
|
-
const isLoading = useAction("addToCart", (s) => s.state === "loading");
|
|
497
|
-
```
|
|
498
|
-
|
|
499
|
-
## State Hooks
|
|
500
|
-
|
|
501
|
-
### useLocationState()
|
|
502
|
-
|
|
503
|
-
Read type-safe state from history:
|
|
504
|
-
|
|
505
|
-
```tsx
|
|
506
|
-
"use client";
|
|
507
|
-
import { useLocationState, createLocationState } from "@rangojs/router/client";
|
|
508
|
-
|
|
509
|
-
// Define typed state (all export patterns supported)
|
|
510
|
-
// Keys are auto-injected by the Vite plugin -- no manual key needed.
|
|
511
|
-
export const ProductState = createLocationState<{
|
|
512
|
-
name: string;
|
|
513
|
-
price: number;
|
|
514
|
-
}>();
|
|
515
|
-
|
|
516
|
-
// Also valid: const ProductState = createLocationState<...>();
|
|
517
|
-
// export { ProductState };
|
|
518
|
-
// Also valid: export { ProductState as MyState };
|
|
519
|
-
|
|
520
|
-
function ProductHeader() {
|
|
521
|
-
const state = useLocationState(ProductState);
|
|
522
|
-
// { name: string; price: number } | undefined
|
|
523
|
-
|
|
524
|
-
if (state) {
|
|
525
|
-
return (
|
|
526
|
-
<h1>
|
|
527
|
-
{state.name} - ${state.price}
|
|
528
|
-
</h1>
|
|
529
|
-
);
|
|
530
|
-
}
|
|
531
|
-
return <h1>Loading...</h1>;
|
|
532
|
-
}
|
|
533
|
-
```
|
|
534
|
-
|
|
535
|
-
Pass state through Link:
|
|
536
|
-
|
|
537
|
-
```tsx
|
|
538
|
-
import { Link } from "@rangojs/router/client";
|
|
539
|
-
import { ProductState } from "./state";
|
|
540
|
-
|
|
541
|
-
<Link to="/product/123" state={[ProductState({ name: "Widget", price: 99 })]}>
|
|
542
|
-
View Product
|
|
543
|
-
</Link>;
|
|
544
|
-
```
|
|
545
|
-
|
|
546
|
-
Pass typed state just in time (getter evaluated at click time, not render time):
|
|
547
|
-
|
|
548
|
-
```tsx
|
|
549
|
-
"use client"; // JIT state requires a client component (getter can't cross RSC boundary)
|
|
550
|
-
|
|
551
|
-
import { Link } from "@rangojs/router/client";
|
|
552
|
-
import { ProductState } from "./state";
|
|
553
|
-
|
|
554
|
-
// The getter is stored lazily and only called when the user clicks the link.
|
|
555
|
-
// This is useful for capturing values that change after render (e.g., scroll
|
|
556
|
-
// position, form state, ref values).
|
|
557
|
-
<Link
|
|
558
|
-
to="/product/123"
|
|
559
|
-
state={[ProductState(() => ({ name: product.name, price: product.price }))]}
|
|
560
|
-
>
|
|
561
|
-
View Product
|
|
562
|
-
</Link>;
|
|
563
|
-
```
|
|
564
|
-
|
|
565
|
-
Plain state can also be evaluated just in time (also requires a client component):
|
|
566
|
-
|
|
567
|
-
```tsx
|
|
568
|
-
<Link to="/product/123" state={() => ({ from: window.location.pathname })}>
|
|
569
|
-
View Product
|
|
570
|
-
</Link>
|
|
571
|
-
```
|
|
572
|
-
|
|
573
|
-
### Flash State (read-once)
|
|
574
|
-
|
|
575
|
-
Create a location state with `{ flash: true }` for read-once state that
|
|
576
|
-
auto-clears after first render. Ideal for flash messages (success/error
|
|
577
|
-
notifications after redirect):
|
|
578
|
-
|
|
579
|
-
```tsx
|
|
580
|
-
// location-states.ts
|
|
581
|
-
import { createLocationState } from "@rangojs/router";
|
|
582
|
-
|
|
583
|
-
export const FlashMessage = createLocationState<{ text: string }>({
|
|
584
|
-
flash: true,
|
|
585
|
-
});
|
|
586
|
-
```
|
|
587
|
-
|
|
588
|
-
Read flash state with `useLocationState` (same hook as persistent state):
|
|
589
|
-
|
|
590
|
-
```tsx
|
|
591
|
-
"use client";
|
|
592
|
-
import { useLocationState } from "@rangojs/router/client";
|
|
593
|
-
import { FlashMessage } from "../location-states";
|
|
594
|
-
|
|
595
|
-
function FlashBanner() {
|
|
596
|
-
const flash = useLocationState(FlashMessage);
|
|
597
|
-
// { text: string } | undefined
|
|
598
|
-
|
|
599
|
-
if (!flash) return null;
|
|
600
|
-
return <div className="flash">{flash.text}</div>;
|
|
601
|
-
}
|
|
602
|
-
```
|
|
603
|
-
|
|
604
|
-
Flash behavior is determined by the definition (`{ flash: true }`), not by which
|
|
605
|
-
hook reads it. `useLocationState` reads the value synchronously during render,
|
|
606
|
-
then clears it from `history.state` via `replaceState` in a `useEffect`.
|
|
607
|
-
Multiple components reading the same flash definition all see the value.
|
|
608
|
-
Pressing back/forward will not re-show the flash since it was cleared.
|
|
609
|
-
|
|
610
|
-
Set flash state from the server via `redirect()` with state:
|
|
611
|
-
|
|
612
|
-
```tsx
|
|
613
|
-
// In a route handler
|
|
614
|
-
import { redirect, createLocationState } from "@rangojs/router";
|
|
615
|
-
|
|
616
|
-
export const FlashMessage = createLocationState<{ text: string }>({
|
|
617
|
-
flash: true,
|
|
618
|
-
});
|
|
619
|
-
|
|
620
|
-
// Handler
|
|
621
|
-
(ctx) => {
|
|
622
|
-
return redirect("/dashboard", {
|
|
623
|
-
state: [FlashMessage({ text: "Item saved!" })],
|
|
624
|
-
});
|
|
625
|
-
};
|
|
626
|
-
```
|
|
627
|
-
|
|
628
|
-
Or via `ctx.setLocationState()` on any response:
|
|
629
|
-
|
|
630
|
-
```tsx
|
|
631
|
-
(ctx) => {
|
|
632
|
-
ctx.setLocationState(FlashMessage({ text: "Welcome back!" }));
|
|
633
|
-
return <Dashboard />;
|
|
634
|
-
};
|
|
635
|
-
```
|
|
636
|
-
|
|
637
|
-
### .read() (non-hook access)
|
|
638
|
-
|
|
639
|
-
Read current location state outside React components (client-side only):
|
|
640
|
-
|
|
641
|
-
```tsx
|
|
642
|
-
import { FlashMessage, ProductState } from "../location-states";
|
|
643
|
-
|
|
644
|
-
// Returns TState | undefined. Returns undefined during SSR.
|
|
645
|
-
const flash = FlashMessage.read();
|
|
646
|
-
const product = ProductState.read();
|
|
647
|
-
```
|
|
648
|
-
|
|
649
|
-
> **Hydration:** `.read()` returns `undefined` on the server but may return
|
|
650
|
-
> a real value on the first client render (history state survives reload).
|
|
651
|
-
> Do not call `.read()` directly during the initial render of a component;
|
|
652
|
-
> call it from an event handler or inside a `useEffect` post-mount. For
|
|
653
|
-
> reactive hydration-safe access, use `useLocationState()` instead.
|
|
654
|
-
|
|
655
|
-
### .write() / .delete() (static, non-reactive)
|
|
656
|
-
|
|
657
|
-
Static counterparts to `.read()`. Both mutate the current history entry's
|
|
658
|
-
`history.state` via `replaceState`, preserving any other keys (router
|
|
659
|
-
bookkeeping, other location state slots). Both are client-only; they throw
|
|
660
|
-
when called on the server.
|
|
661
|
-
|
|
662
|
-
Neither dispatches an event, so components reading via `useLocationState`
|
|
663
|
-
will NOT re-render until the next navigation/popstate. Pair with `.read()`
|
|
664
|
-
(or a fresh mount via back/forward/reload) instead.
|
|
665
|
-
|
|
666
|
-
```tsx
|
|
667
|
-
"use client";
|
|
668
|
-
import { ProductState } from "./state";
|
|
669
|
-
|
|
670
|
-
// Persisted across hard refresh and back/forward of this entry.
|
|
671
|
-
ProductState.write({ name: "Widget", price: 9.99 });
|
|
672
|
-
|
|
673
|
-
// Read later (or on next mount).
|
|
674
|
-
const current = ProductState.read();
|
|
675
|
-
|
|
676
|
-
// Manually clear the slot. Idempotent if it isn't set.
|
|
677
|
-
ProductState.delete();
|
|
678
|
-
```
|
|
679
|
-
|
|
680
|
-
| Method | Updates `history.state` | Fires `useLocationState` rerender | SSR behavior |
|
|
681
|
-
| ----------- | ----------------------- | --------------------------------- | ------------------- |
|
|
682
|
-
| `.read()` | no | n/a (returns snapshot) | returns `undefined` |
|
|
683
|
-
| `.write()` | yes (replace this slot) | no | throws |
|
|
684
|
-
| `.delete()` | yes (remove this slot) | no | throws |
|
|
685
|
-
|
|
686
|
-
## Cache Control
|
|
687
|
-
|
|
688
|
-
### invalidateClientCache()
|
|
689
|
-
|
|
690
|
-
Force the client's caches to miss after a mutation the router can't see (a REST
|
|
691
|
-
call, a WebSocket push, a login). It is a plain function, not a hook, so it works
|
|
692
|
-
from module-level callbacks too. Imported from the root entry `@rangojs/router`,
|
|
693
|
-
it is selected by export conditions: in a client component it marks the caches
|
|
694
|
-
stale immediately; from a handler/server component it writes a rotated
|
|
695
|
-
`Set-Cookie` for the responding client.
|
|
696
|
-
|
|
697
|
-
```tsx
|
|
698
|
-
"use client";
|
|
699
|
-
import { invalidateClientCache } from "@rangojs/router";
|
|
700
|
-
|
|
701
|
-
function SaveButton() {
|
|
702
|
-
const handleSave = async () => {
|
|
703
|
-
await fetch("/api/data", {
|
|
704
|
-
method: "POST",
|
|
705
|
-
body: JSON.stringify(data),
|
|
706
|
-
});
|
|
707
|
-
|
|
708
|
-
// Invalidate the client's caches after the mutation
|
|
709
|
-
invalidateClientCache();
|
|
710
|
-
};
|
|
711
|
-
|
|
712
|
-
return <button onClick={handleSave}>Save</button>;
|
|
713
|
-
}
|
|
714
|
-
```
|
|
715
|
-
|
|
716
|
-
A module-level subscription works the same way (no component needed):
|
|
717
|
-
|
|
718
|
-
```ts
|
|
719
|
-
import { invalidateClientCache } from "@rangojs/router";
|
|
720
|
-
|
|
721
|
-
socket.on("catalog-updated", () => invalidateClientCache());
|
|
722
|
-
```
|
|
723
|
-
|
|
724
|
-
**Use cases**: REST API mutations, WebSocket updates, non-RSC data changes.
|
|
725
|
-
|
|
726
|
-
## Outlet Components
|
|
727
|
-
|
|
728
|
-
### Outlet / ParallelOutlet
|
|
729
|
-
|
|
730
|
-
Render child content in layouts:
|
|
731
|
-
|
|
732
|
-
```tsx
|
|
733
|
-
import { Outlet, ParallelOutlet } from "@rangojs/router/client";
|
|
734
|
-
|
|
735
|
-
function DashboardLayout({ children }: { children?: React.ReactNode }) {
|
|
736
|
-
return (
|
|
737
|
-
<div className="dashboard">
|
|
738
|
-
<aside>
|
|
739
|
-
<ParallelOutlet name="@sidebar" />
|
|
740
|
-
</aside>
|
|
741
|
-
<main>{children ?? <Outlet />}</main>
|
|
742
|
-
<ParallelOutlet name="@notifications" />
|
|
743
|
-
</div>
|
|
744
|
-
);
|
|
745
|
-
}
|
|
746
|
-
```
|
|
747
|
-
|
|
748
|
-
### useOutlet()
|
|
749
|
-
|
|
750
|
-
Access outlet content programmatically:
|
|
751
|
-
|
|
752
|
-
```tsx
|
|
753
|
-
"use client";
|
|
754
|
-
import { useOutlet } from "@rangojs/router/client";
|
|
755
|
-
|
|
756
|
-
function ConditionalLayout() {
|
|
757
|
-
const outlet = useOutlet();
|
|
758
|
-
// ReactNode | null
|
|
759
|
-
|
|
760
|
-
return outlet ? (
|
|
761
|
-
<div className="with-content">{outlet}</div>
|
|
762
|
-
) : (
|
|
763
|
-
<div className="empty">No content</div>
|
|
764
|
-
);
|
|
765
|
-
}
|
|
766
|
-
```
|
|
767
|
-
|
|
768
|
-
## URL Hooks
|
|
769
|
-
|
|
770
|
-
### useParams()
|
|
771
|
-
|
|
772
|
-
Access route params from the current URL:
|
|
773
|
-
|
|
774
|
-
```tsx
|
|
775
|
-
"use client";
|
|
776
|
-
import { useParams } from "@rangojs/router/client";
|
|
777
|
-
|
|
778
|
-
// Route: /product/:productId
|
|
779
|
-
function ProductPage() {
|
|
780
|
-
const params = useParams();
|
|
781
|
-
// { productId: "123" }
|
|
782
|
-
|
|
783
|
-
return <h1>Product {params.productId}</h1>;
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
// Annotate the expected shape via a generic
|
|
787
|
-
function ProductPageTyped() {
|
|
788
|
-
const { productId } = useParams<{ productId: string }>();
|
|
789
|
-
return <h1>Product {productId}</h1>;
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
// With selector for performance (re-renders only when selected value changes)
|
|
793
|
-
function ProductId() {
|
|
794
|
-
const productId = useParams((p) => p.productId);
|
|
795
|
-
return <span>ID: {productId}</span>;
|
|
796
|
-
}
|
|
797
|
-
```
|
|
798
|
-
|
|
799
|
-
Returns merged params from all matched route segments as a `Readonly<T>` map. Updates on navigation commit (not during pending navigation).
|
|
800
|
-
|
|
801
|
-
### usePathname()
|
|
802
|
-
|
|
803
|
-
Access the current URL pathname:
|
|
804
|
-
|
|
805
|
-
```tsx
|
|
806
|
-
"use client";
|
|
807
|
-
import { usePathname } from "@rangojs/router/client";
|
|
808
|
-
|
|
809
|
-
function CurrentPage() {
|
|
810
|
-
const pathname = usePathname();
|
|
811
|
-
// "/product/123" (no search params)
|
|
812
|
-
|
|
813
|
-
return <span>Current path: {pathname}</span>;
|
|
814
|
-
}
|
|
815
|
-
```
|
|
816
|
-
|
|
817
|
-
Returns the pathname string without search params or hash. Updates on navigation commit.
|
|
818
|
-
|
|
819
|
-
### useSearchParams()
|
|
820
|
-
|
|
821
|
-
Access the current URL search params:
|
|
822
|
-
|
|
823
|
-
```tsx
|
|
824
|
-
"use client";
|
|
825
|
-
import { useSearchParams } from "@rangojs/router/client";
|
|
826
|
-
|
|
827
|
-
function SearchResults() {
|
|
828
|
-
const searchParams = useSearchParams();
|
|
829
|
-
const query = searchParams.get("q"); // "react"
|
|
830
|
-
const page = searchParams.get("page"); // "2"
|
|
831
|
-
|
|
832
|
-
return (
|
|
833
|
-
<div>
|
|
834
|
-
Searching for: {query}, page {page}
|
|
835
|
-
</div>
|
|
836
|
-
);
|
|
837
|
-
}
|
|
838
|
-
```
|
|
839
|
-
|
|
840
|
-
Returns a `ReadonlyURLSearchParams` (URLSearchParams without mutation methods). During SSR, returns empty params and syncs from the browser URL on mount.
|
|
841
|
-
|
|
842
|
-
### useHref()
|
|
843
|
-
|
|
844
|
-
Mount-aware href for client components inside `include()` scopes:
|
|
845
|
-
|
|
846
|
-
```tsx
|
|
847
|
-
"use client";
|
|
848
|
-
import { useHref, href, Link } from "@rangojs/router/client";
|
|
849
|
-
|
|
850
|
-
// Inside include("/shop", shopPatterns)
|
|
851
|
-
function ShopNav() {
|
|
852
|
-
const href = useHref();
|
|
853
|
-
|
|
854
|
-
return (
|
|
855
|
-
<>
|
|
856
|
-
{/* Local paths - auto-prefixed with /shop */}
|
|
857
|
-
<Link to={href("/cart")}>Cart</Link>
|
|
858
|
-
<Link to={href("/product/widget")}>Widget</Link>
|
|
859
|
-
</>
|
|
860
|
-
);
|
|
861
|
-
}
|
|
862
|
-
```
|
|
863
|
-
|
|
864
|
-
Use `useHref()` for local navigation. Use the bare `href()` function for absolute paths.
|
|
865
|
-
|
|
866
|
-
### useMount()
|
|
867
|
-
|
|
868
|
-
Returns the current `include()` mount path:
|
|
869
|
-
|
|
870
|
-
```tsx
|
|
871
|
-
"use client";
|
|
872
|
-
import { useMount } from "@rangojs/router/client";
|
|
873
|
-
|
|
874
|
-
function MountInfo() {
|
|
875
|
-
const mount = useMount(); // "/shop" inside include("/shop", ...)
|
|
876
|
-
return <span>Mounted at: {mount}</span>;
|
|
877
|
-
}
|
|
878
|
-
```
|
|
879
|
-
|
|
880
|
-
### useReverse(routes)
|
|
881
|
-
|
|
882
|
-
Mount-aware local reverse for client components. Import the generated `routes` map from a `urls()` module's `.gen.ts` and call `reverse("name", params?)` — the leading dot is optional. Auto-fills params from `useParams()`; explicit params override.
|
|
883
|
-
|
|
884
|
-
> Per-module `*.gen.ts` files are **CLI opt-in and not Vite-watched** — run `rango generate <urls-file>` (or wire it into `predev`) and re-run it whenever the module's routes change. See `/links` for the full generated-file setup and exposure-boundary rules.
|
|
885
|
-
|
|
886
|
-
```tsx
|
|
887
|
-
"use client";
|
|
888
|
-
import { Link, useReverse } from "@rangojs/router/client";
|
|
889
|
-
import { routes as blogRoutes } from "../urls/blog.gen.js";
|
|
890
|
-
|
|
891
|
-
function BlogNav() {
|
|
892
|
-
const reverse = useReverse(blogRoutes);
|
|
893
|
-
return (
|
|
894
|
-
<nav>
|
|
895
|
-
<Link to={reverse("index")}>Blog</Link>
|
|
896
|
-
<Link to={reverse("post", { postId: "hello" })}>Post</Link>
|
|
897
|
-
</nav>
|
|
898
|
-
);
|
|
899
|
-
}
|
|
900
|
-
```
|
|
901
|
-
|
|
902
|
-
See `/links` for the full URL generation guide. `ctx.reverse()` is server-only; on the client, prefer `useReverse(routes)` for in-module names and pass URLs as props for cross-module ones.
|
|
12
|
+
## Not this skill if…
|
|
13
|
+
|
|
14
|
+
- You want to fetch data — data fetching happens in server-side loaders: see
|
|
15
|
+
`/loader`. Client hooks like `useLoader()` only consume what a loader
|
|
16
|
+
resolved.
|
|
17
|
+
- You want to control when a loader re-runs after an action — that is
|
|
18
|
+
`revalidate()` in the server DSL: see `/loader`.
|
|
19
|
+
|
|
20
|
+
Each hook's full API and recipe lives in a companion file linked below. Read
|
|
21
|
+
the one for your case.
|
|
22
|
+
|
|
23
|
+
## Decision table
|
|
24
|
+
|
|
25
|
+
| I need... | Hook | File |
|
|
26
|
+
| -------------------------------------------------- | --------------------------- | ---------------------------------------------------- |
|
|
27
|
+
| Reactive navigation state | `useNavigation()` | [`./navigation.md`](./navigation.md) |
|
|
28
|
+
| Stable router actions (push/replace/refresh/…) | `useRouter()` | [`./navigation.md`](./navigation.md) |
|
|
29
|
+
| Current URL path & matched segments | `useSegments()` | [`./navigation.md`](./navigation.md) |
|
|
30
|
+
| Pending state inside a `<Link>` | `useLinkStatus()` | [`./navigation.md`](./navigation.md) |
|
|
31
|
+
| Loader data (strict, guaranteed) | `useLoader()` | [`./data.md`](./data.md) |
|
|
32
|
+
| Loader data with on-demand fetch | `useFetchLoader()` | [`./data.md`](./data.md) |
|
|
33
|
+
| Refresh multiple loaders across groups | `useRefreshLoaders()` | [`./data.md`](./data.md) |
|
|
34
|
+
| Accumulated handle data from route segments | `useHandle()` | [`./handle-and-actions.md`](./handle-and-actions.md) |
|
|
35
|
+
| Server action invocation state | `useAction()` | [`./handle-and-actions.md`](./handle-and-actions.md) |
|
|
36
|
+
| Type-safe history state (persistent or flash) | `useLocationState()` | [`./state.md`](./state.md) |
|
|
37
|
+
| Force the client's caches to miss after a mutation | `invalidateClientCache()` | [`./state.md`](./state.md) |
|
|
38
|
+
| Render child content in a layout | `Outlet` / `ParallelOutlet` | [`./outlets.md`](./outlets.md) |
|
|
39
|
+
| Access outlet content programmatically | `useOutlet()` | [`./outlets.md`](./outlets.md) |
|
|
40
|
+
| Route params from the current URL | `useParams()` | [`./urls.md`](./urls.md) |
|
|
41
|
+
| Current URL pathname | `usePathname()` | [`./urls.md`](./urls.md) |
|
|
42
|
+
| Current URL search params | `useSearchParams()` | [`./urls.md`](./urls.md) |
|
|
43
|
+
| Mount-aware href inside an `include()` scope | `useHref()` | [`./urls.md`](./urls.md) |
|
|
44
|
+
| Current `include()` mount path | `useMount()` | [`./urls.md`](./urls.md) |
|
|
45
|
+
| Local reverse for an imported `urls()` routes map | `useReverse(routes)` | [`./urls.md`](./urls.md) |
|
|
46
|
+
|
|
47
|
+
## Companion files
|
|
48
|
+
|
|
49
|
+
- [`./navigation.md`](./navigation.md) — `useNavigation`, `useRouter` (incl.
|
|
50
|
+
`revalidate: false`), `useSegments`, `useLinkStatus`.
|
|
51
|
+
- [`./data.md`](./data.md) — `useLoader`, `useFetchLoader` (shared refetch
|
|
52
|
+
scoping, `key`, `refreshGroup` + `useRefreshLoaders`, load options, file
|
|
53
|
+
uploads).
|
|
54
|
+
- [`./handle-and-actions.md`](./handle-and-actions.md) — `useHandle`,
|
|
55
|
+
`useAction`. For the full server-action guide (defining actions,
|
|
56
|
+
`useActionState`, `useOptimistic`, validation, revalidation, error
|
|
57
|
+
handling, file uploads), see `/server-actions`; `useAction()` here is the
|
|
58
|
+
Rango-specific hook for tracking actions called outside a
|
|
59
|
+
`<form action={...}>` flow.
|
|
60
|
+
- [`./state.md`](./state.md) — `useLocationState` (persistent + flash state,
|
|
61
|
+
`.read()`/`.write()`/`.delete()`), `invalidateClientCache()`.
|
|
62
|
+
- [`./outlets.md`](./outlets.md) — `Outlet`, `ParallelOutlet`, `useOutlet`.
|
|
63
|
+
- [`./urls.md`](./urls.md) — `useParams`, `usePathname`, `useSearchParams`,
|
|
64
|
+
`useHref`, `useMount`, `useReverse`.
|
|
903
65
|
|
|
904
66
|
## Hook Summary
|
|
905
67
|
|