@rangojs/router 0.0.0-experimental.10
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/CLAUDE.md +43 -0
- package/README.md +19 -0
- package/dist/bin/rango.js +227 -0
- package/dist/vite/index.js +3039 -0
- package/package.json +171 -0
- package/skills/caching/SKILL.md +191 -0
- package/skills/debug-manifest/SKILL.md +108 -0
- package/skills/document-cache/SKILL.md +180 -0
- package/skills/fonts/SKILL.md +165 -0
- package/skills/hooks/SKILL.md +442 -0
- package/skills/intercept/SKILL.md +190 -0
- package/skills/layout/SKILL.md +213 -0
- package/skills/links/SKILL.md +180 -0
- package/skills/loader/SKILL.md +246 -0
- package/skills/middleware/SKILL.md +202 -0
- package/skills/mime-routes/SKILL.md +124 -0
- package/skills/parallel/SKILL.md +228 -0
- package/skills/prerender/SKILL.md +283 -0
- package/skills/rango/SKILL.md +54 -0
- package/skills/response-routes/SKILL.md +358 -0
- package/skills/route/SKILL.md +173 -0
- package/skills/router-setup/SKILL.md +346 -0
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +78 -0
- package/skills/typesafety/SKILL.md +394 -0
- package/src/__internal.ts +175 -0
- package/src/bin/rango.ts +24 -0
- package/src/browser/event-controller.ts +876 -0
- package/src/browser/index.ts +18 -0
- package/src/browser/link-interceptor.ts +121 -0
- package/src/browser/lru-cache.ts +69 -0
- package/src/browser/merge-segment-loaders.ts +126 -0
- package/src/browser/navigation-bridge.ts +913 -0
- package/src/browser/navigation-client.ts +165 -0
- package/src/browser/navigation-store.ts +823 -0
- package/src/browser/partial-update.ts +600 -0
- package/src/browser/react/Link.tsx +248 -0
- package/src/browser/react/NavigationProvider.tsx +346 -0
- package/src/browser/react/ScrollRestoration.tsx +94 -0
- package/src/browser/react/context.ts +53 -0
- package/src/browser/react/index.ts +52 -0
- package/src/browser/react/location-state-shared.ts +120 -0
- package/src/browser/react/location-state.ts +62 -0
- package/src/browser/react/mount-context.ts +32 -0
- package/src/browser/react/use-action.ts +240 -0
- package/src/browser/react/use-client-cache.ts +56 -0
- package/src/browser/react/use-handle.ts +203 -0
- package/src/browser/react/use-href.tsx +40 -0
- package/src/browser/react/use-link-status.ts +134 -0
- package/src/browser/react/use-mount.ts +31 -0
- package/src/browser/react/use-navigation.ts +140 -0
- package/src/browser/react/use-segments.ts +188 -0
- package/src/browser/request-controller.ts +164 -0
- package/src/browser/rsc-router.tsx +352 -0
- package/src/browser/scroll-restoration.ts +324 -0
- package/src/browser/segment-structure-assert.ts +67 -0
- package/src/browser/server-action-bridge.ts +762 -0
- package/src/browser/shallow.ts +35 -0
- package/src/browser/types.ts +478 -0
- package/src/build/generate-manifest.ts +377 -0
- package/src/build/generate-route-types.ts +828 -0
- package/src/build/index.ts +36 -0
- package/src/build/route-trie.ts +239 -0
- package/src/cache/cache-scope.ts +563 -0
- package/src/cache/cf/cf-cache-store.ts +428 -0
- package/src/cache/cf/index.ts +19 -0
- package/src/cache/document-cache.ts +340 -0
- package/src/cache/index.ts +58 -0
- package/src/cache/memory-segment-store.ts +150 -0
- package/src/cache/memory-store.ts +253 -0
- package/src/cache/types.ts +392 -0
- package/src/client.rsc.tsx +83 -0
- package/src/client.tsx +643 -0
- package/src/component-utils.ts +76 -0
- package/src/components/DefaultDocument.tsx +23 -0
- package/src/debug.ts +233 -0
- package/src/default-error-boundary.tsx +88 -0
- package/src/deps/browser.ts +8 -0
- package/src/deps/html-stream-client.ts +2 -0
- package/src/deps/html-stream-server.ts +2 -0
- package/src/deps/rsc.ts +10 -0
- package/src/deps/ssr.ts +2 -0
- package/src/errors.ts +295 -0
- package/src/handle.ts +130 -0
- package/src/handles/MetaTags.tsx +193 -0
- package/src/handles/index.ts +6 -0
- package/src/handles/meta.ts +247 -0
- package/src/host/cookie-handler.ts +159 -0
- package/src/host/errors.ts +97 -0
- package/src/host/index.ts +56 -0
- package/src/host/pattern-matcher.ts +214 -0
- package/src/host/router.ts +330 -0
- package/src/host/testing.ts +79 -0
- package/src/host/types.ts +138 -0
- package/src/host/utils.ts +25 -0
- package/src/href-client.ts +202 -0
- package/src/href-context.ts +33 -0
- package/src/index.rsc.ts +121 -0
- package/src/index.ts +165 -0
- package/src/loader.rsc.ts +207 -0
- package/src/loader.ts +47 -0
- package/src/network-error-thrower.tsx +21 -0
- package/src/outlet-context.ts +15 -0
- package/src/prerender/param-hash.ts +35 -0
- package/src/prerender/store.ts +40 -0
- package/src/prerender.ts +156 -0
- package/src/reverse.ts +267 -0
- package/src/root-error-boundary.tsx +277 -0
- package/src/route-content-wrapper.tsx +193 -0
- package/src/route-definition.ts +1431 -0
- package/src/route-map-builder.ts +242 -0
- package/src/route-types.ts +220 -0
- package/src/router/error-handling.ts +287 -0
- package/src/router/handler-context.ts +158 -0
- package/src/router/intercept-resolution.ts +387 -0
- package/src/router/loader-resolution.ts +327 -0
- package/src/router/manifest.ts +216 -0
- package/src/router/match-api.ts +621 -0
- package/src/router/match-context.ts +264 -0
- package/src/router/match-middleware/background-revalidation.ts +236 -0
- package/src/router/match-middleware/cache-lookup.ts +382 -0
- package/src/router/match-middleware/cache-store.ts +276 -0
- package/src/router/match-middleware/index.ts +81 -0
- package/src/router/match-middleware/intercept-resolution.ts +281 -0
- package/src/router/match-middleware/segment-resolution.ts +184 -0
- package/src/router/match-pipelines.ts +214 -0
- package/src/router/match-result.ts +213 -0
- package/src/router/metrics.ts +62 -0
- package/src/router/middleware.ts +791 -0
- package/src/router/pattern-matching.ts +407 -0
- package/src/router/revalidation.ts +190 -0
- package/src/router/router-context.ts +301 -0
- package/src/router/segment-resolution.ts +1315 -0
- package/src/router/trie-matching.ts +172 -0
- package/src/router/types.ts +163 -0
- package/src/router.gen.ts +6 -0
- package/src/router.ts +2423 -0
- package/src/rsc/handler.ts +1443 -0
- package/src/rsc/helpers.ts +64 -0
- package/src/rsc/index.ts +56 -0
- package/src/rsc/nonce.ts +18 -0
- package/src/rsc/types.ts +236 -0
- package/src/segment-system.tsx +442 -0
- package/src/server/context.ts +466 -0
- package/src/server/handle-store.ts +229 -0
- package/src/server/loader-registry.ts +174 -0
- package/src/server/request-context.ts +554 -0
- package/src/server/root-layout.tsx +10 -0
- package/src/server/tsconfig.json +14 -0
- package/src/server.ts +171 -0
- package/src/ssr/index.tsx +296 -0
- package/src/theme/ThemeProvider.tsx +291 -0
- package/src/theme/ThemeScript.tsx +61 -0
- package/src/theme/constants.ts +59 -0
- package/src/theme/index.ts +58 -0
- package/src/theme/theme-context.ts +70 -0
- package/src/theme/theme-script.ts +152 -0
- package/src/theme/types.ts +182 -0
- package/src/theme/use-theme.ts +44 -0
- package/src/types.ts +1757 -0
- package/src/urls.gen.ts +8 -0
- package/src/urls.ts +1282 -0
- package/src/use-loader.tsx +346 -0
- package/src/vite/expose-action-id.ts +344 -0
- package/src/vite/expose-handle-id.ts +209 -0
- package/src/vite/expose-loader-id.ts +426 -0
- package/src/vite/expose-location-state-id.ts +177 -0
- package/src/vite/expose-prerender-handler-id.ts +429 -0
- package/src/vite/index.ts +2068 -0
- package/src/vite/package-resolution.ts +125 -0
- package/src/vite/version.d.ts +12 -0
- package/src/vite/virtual-entries.ts +114 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: fonts
|
|
3
|
+
description: Load and configure web fonts with preload hints for optimal performance
|
|
4
|
+
argument-hint: [provider]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Fonts
|
|
8
|
+
|
|
9
|
+
Load web fonts in the Document component with `<link rel="preload">` for optimal performance. Fonts are declared in `<head>` alongside your stylesheet.
|
|
10
|
+
|
|
11
|
+
## Google Fonts
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
// src/document.tsx
|
|
15
|
+
"use client";
|
|
16
|
+
|
|
17
|
+
import type { ReactNode } from "react";
|
|
18
|
+
import { MetaTags } from "@rangojs/router/client";
|
|
19
|
+
import styles from "./index.css?url";
|
|
20
|
+
|
|
21
|
+
export function Document({ children }: { children: ReactNode }) {
|
|
22
|
+
return (
|
|
23
|
+
<html lang="en">
|
|
24
|
+
<head>
|
|
25
|
+
{/* Preconnect to Google Fonts */}
|
|
26
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
27
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
|
28
|
+
|
|
29
|
+
{/* Load font stylesheet */}
|
|
30
|
+
<link
|
|
31
|
+
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
|
32
|
+
rel="stylesheet"
|
|
33
|
+
/>
|
|
34
|
+
|
|
35
|
+
{/* App styles */}
|
|
36
|
+
<link rel="preload" href={styles} as="style" />
|
|
37
|
+
<link rel="stylesheet" href={styles} />
|
|
38
|
+
<MetaTags />
|
|
39
|
+
</head>
|
|
40
|
+
<body>
|
|
41
|
+
{children}
|
|
42
|
+
</body>
|
|
43
|
+
</html>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Then reference the font in CSS:
|
|
49
|
+
|
|
50
|
+
```css
|
|
51
|
+
/* src/index.css */
|
|
52
|
+
body {
|
|
53
|
+
font-family: "Inter", sans-serif;
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Or with Tailwind (see `/tailwind`):
|
|
58
|
+
|
|
59
|
+
```css
|
|
60
|
+
/* src/index.css */
|
|
61
|
+
@theme {
|
|
62
|
+
--font-sans: "Inter", sans-serif;
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Self-Hosted Fonts
|
|
67
|
+
|
|
68
|
+
Place font files in `public/fonts/` and use `@font-face`:
|
|
69
|
+
|
|
70
|
+
```css
|
|
71
|
+
/* src/index.css */
|
|
72
|
+
@font-face {
|
|
73
|
+
font-family: "CustomFont";
|
|
74
|
+
src: url("/fonts/custom-regular.woff2") format("woff2");
|
|
75
|
+
font-weight: 400;
|
|
76
|
+
font-style: normal;
|
|
77
|
+
font-display: swap;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@font-face {
|
|
81
|
+
font-family: "CustomFont";
|
|
82
|
+
src: url("/fonts/custom-bold.woff2") format("woff2");
|
|
83
|
+
font-weight: 700;
|
|
84
|
+
font-style: normal;
|
|
85
|
+
font-display: swap;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
body {
|
|
89
|
+
font-family: "CustomFont", sans-serif;
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Preload the most critical weight in the Document:
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
export function Document({ children }: { children: ReactNode }) {
|
|
97
|
+
return (
|
|
98
|
+
<html lang="en">
|
|
99
|
+
<head>
|
|
100
|
+
<link
|
|
101
|
+
rel="preload"
|
|
102
|
+
href="/fonts/custom-regular.woff2"
|
|
103
|
+
as="font"
|
|
104
|
+
type="font/woff2"
|
|
105
|
+
crossOrigin="anonymous"
|
|
106
|
+
/>
|
|
107
|
+
<link rel="preload" href={styles} as="style" />
|
|
108
|
+
<link rel="stylesheet" href={styles} />
|
|
109
|
+
<MetaTags />
|
|
110
|
+
</head>
|
|
111
|
+
<body>{children}</body>
|
|
112
|
+
</html>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Fontsource (Recommended for Vite)
|
|
118
|
+
|
|
119
|
+
`@fontsource-variable` packages are the recommended approach with Vite. Fonts are installed as npm dependencies, bundled by Vite, and served from your own domain -- no external requests, no privacy concerns, no FOUT from slow CDNs.
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
pnpm add @fontsource-variable/inter
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Import the font CSS in your stylesheet. Vite resolves the `@fontsource-variable` import from `node_modules` and bundles the woff2 files as hashed assets automatically:
|
|
126
|
+
|
|
127
|
+
```css
|
|
128
|
+
/* src/index.css */
|
|
129
|
+
@import "@fontsource-variable/inter";
|
|
130
|
+
|
|
131
|
+
body {
|
|
132
|
+
font-family: "Inter Variable", sans-serif;
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
With Tailwind:
|
|
137
|
+
|
|
138
|
+
```css
|
|
139
|
+
/* src/index.css */
|
|
140
|
+
@import "@fontsource-variable/inter";
|
|
141
|
+
@import "tailwindcss";
|
|
142
|
+
|
|
143
|
+
@theme {
|
|
144
|
+
--font-sans: "Inter Variable", sans-serif;
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Why this is preferred over Google Fonts with Vite:
|
|
149
|
+
|
|
150
|
+
- No external network requests at runtime -- fonts are bundled into your build output
|
|
151
|
+
- No `<link rel="preconnect">` or extra stylesheet needed in the Document
|
|
152
|
+
- Variable font = single file covers all weights, smaller total download
|
|
153
|
+
- Vite handles cache-busting via content hashes
|
|
154
|
+
- Works offline and in edge deployments (Cloudflare Workers) without external dependencies
|
|
155
|
+
|
|
156
|
+
Browse available fonts at fontsource.org. Use `@fontsource-variable/*` for variable fonts and `@fontsource/*` for static fonts.
|
|
157
|
+
|
|
158
|
+
## Performance Tips
|
|
159
|
+
|
|
160
|
+
- Prefer `@fontsource-variable` with Vite for self-hosted, zero-config font loading
|
|
161
|
+
- Use `font-display: swap` to prevent invisible text during font load
|
|
162
|
+
- Preload only the most critical font weight (usually regular 400)
|
|
163
|
+
- Prefer `woff2` format for smaller file sizes
|
|
164
|
+
- Use variable fonts when multiple weights are needed to reduce total file count
|
|
165
|
+
- `<link rel="preconnect">` eliminates DNS + TLS latency for external font providers
|
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: hooks
|
|
3
|
+
description: Client-side React hooks for navigation, loaders, and state in @rangojs/router
|
|
4
|
+
argument-hint: [hook-name]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Client-Side React Hooks
|
|
8
|
+
|
|
9
|
+
All hooks are imported from `@rangojs/router` or `@rangojs/router/client`.
|
|
10
|
+
|
|
11
|
+
## Navigation Hooks
|
|
12
|
+
|
|
13
|
+
### useNavigation()
|
|
14
|
+
|
|
15
|
+
Track navigation state and control navigation:
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
"use client";
|
|
19
|
+
import { useNavigation } from "@rangojs/router";
|
|
20
|
+
|
|
21
|
+
function NavIndicator() {
|
|
22
|
+
const nav = useNavigation();
|
|
23
|
+
|
|
24
|
+
// Full state
|
|
25
|
+
nav.state; // 'idle' | 'loading' | 'streaming'
|
|
26
|
+
nav.isStreaming; // boolean
|
|
27
|
+
nav.location; // Current URL
|
|
28
|
+
nav.pendingUrl; // Target URL during navigation (or null)
|
|
29
|
+
|
|
30
|
+
// Methods
|
|
31
|
+
nav.navigate("/products"); // Navigate programmatically
|
|
32
|
+
nav.navigate("/products", { replace: true }); // Replace history
|
|
33
|
+
nav.refresh(); // Refresh current route
|
|
34
|
+
|
|
35
|
+
return nav.state === 'loading' ? <Spinner /> : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// With selector for performance
|
|
39
|
+
function IsLoading() {
|
|
40
|
+
const isLoading = useNavigation(nav => nav.state === 'loading');
|
|
41
|
+
return isLoading ? <Spinner /> : null;
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### useSegments()
|
|
46
|
+
|
|
47
|
+
Access current URL path and matched route segments:
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
"use client";
|
|
51
|
+
import { useSegments } from "@rangojs/router";
|
|
52
|
+
|
|
53
|
+
function Breadcrumbs() {
|
|
54
|
+
const { path, segmentIds, location } = useSegments();
|
|
55
|
+
|
|
56
|
+
// path: ["/shop", "products", "123"]
|
|
57
|
+
// segmentIds: ["shop-layout", "products-route"]
|
|
58
|
+
// location: URL object
|
|
59
|
+
|
|
60
|
+
return <nav>{path.join(" > ")}</nav>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// With selector
|
|
64
|
+
const isShopRoute = useSegments(s => s.path[0] === "shop");
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### useLinkStatus()
|
|
68
|
+
|
|
69
|
+
Track pending state inside a Link component:
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
"use client";
|
|
73
|
+
import { Link, useLinkStatus } from "@rangojs/router/client";
|
|
74
|
+
|
|
75
|
+
function LoadingIndicator() {
|
|
76
|
+
const { pending } = useLinkStatus();
|
|
77
|
+
return pending ? <Spinner /> : null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Must be inside Link
|
|
81
|
+
<Link to="/dashboard">
|
|
82
|
+
Dashboard
|
|
83
|
+
<LoadingIndicator />
|
|
84
|
+
</Link>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Data Hooks
|
|
88
|
+
|
|
89
|
+
### useLoader()
|
|
90
|
+
|
|
91
|
+
Access loader data (strict - data guaranteed):
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
"use client";
|
|
95
|
+
import { useLoader } from "@rangojs/router";
|
|
96
|
+
import { ProductLoader } from "../loaders/product";
|
|
97
|
+
|
|
98
|
+
function ProductPrice() {
|
|
99
|
+
const { data, isLoading, error } = useLoader(ProductLoader);
|
|
100
|
+
|
|
101
|
+
// data: T (guaranteed - throws if not in context)
|
|
102
|
+
// isLoading: boolean
|
|
103
|
+
// error: Error | null
|
|
104
|
+
|
|
105
|
+
return <span>${data.price}</span>;
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**Precondition**: Loader must be registered on route via `loader()` helper.
|
|
110
|
+
|
|
111
|
+
Loaders can also be passed as props from server to client components:
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
"use client";
|
|
115
|
+
import { useLoader } from "@rangojs/router/client";
|
|
116
|
+
import type { ProductLoader } from "../loaders";
|
|
117
|
+
|
|
118
|
+
// typeof infers the full data type from the loader definition
|
|
119
|
+
function ProductCard({ loader }: { loader: typeof ProductLoader }) {
|
|
120
|
+
const { data } = useLoader(loader);
|
|
121
|
+
return <h2>{data.product.name}</h2>;
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### useFetchLoader()
|
|
126
|
+
|
|
127
|
+
Access loader with on-demand fetching (flexible):
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
"use client";
|
|
131
|
+
import { useFetchLoader } from "@rangojs/router";
|
|
132
|
+
import { SearchLoader } from "../loaders/search";
|
|
133
|
+
|
|
134
|
+
function SearchResults() {
|
|
135
|
+
const { data, load, isLoading, error } = useFetchLoader(SearchLoader);
|
|
136
|
+
|
|
137
|
+
// data: T | undefined (may be undefined if not fetched)
|
|
138
|
+
// load: (options?) => Promise<T>
|
|
139
|
+
// refetch: alias for load
|
|
140
|
+
|
|
141
|
+
const handleSearch = async (query: string) => {
|
|
142
|
+
await load({ params: { query } });
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<div>
|
|
147
|
+
<input onChange={(e) => handleSearch(e.target.value)} />
|
|
148
|
+
{isLoading && <Spinner />}
|
|
149
|
+
{data?.results.map(r => <Result key={r.id} {...r} />)}
|
|
150
|
+
</div>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**Load options**:
|
|
156
|
+
```tsx
|
|
157
|
+
await load({
|
|
158
|
+
method: 'POST', // GET, POST, PUT, PATCH, DELETE
|
|
159
|
+
params: { query: 'test' }, // Query string (GET) or body (others)
|
|
160
|
+
body: { data: 'value' }, // For POST/PUT/PATCH/DELETE
|
|
161
|
+
});
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### useLoaderData()
|
|
165
|
+
|
|
166
|
+
Get all loader data in current context:
|
|
167
|
+
|
|
168
|
+
```tsx
|
|
169
|
+
"use client";
|
|
170
|
+
import { useLoaderData } from "@rangojs/router";
|
|
171
|
+
|
|
172
|
+
function DebugPanel() {
|
|
173
|
+
const allData = useLoaderData();
|
|
174
|
+
// Record<string, any> - Map of loader ID to data
|
|
175
|
+
|
|
176
|
+
return <pre>{JSON.stringify(allData, null, 2)}</pre>;
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Handle Hooks
|
|
181
|
+
|
|
182
|
+
### useHandle()
|
|
183
|
+
|
|
184
|
+
Access accumulated handle data from route segments:
|
|
185
|
+
|
|
186
|
+
```tsx
|
|
187
|
+
"use client";
|
|
188
|
+
import { useHandle } from "@rangojs/router";
|
|
189
|
+
import { Breadcrumbs } from "../handles/breadcrumbs";
|
|
190
|
+
|
|
191
|
+
function BreadcrumbNav() {
|
|
192
|
+
const crumbs = useHandle(Breadcrumbs);
|
|
193
|
+
// Array of { label, href } accumulated from layouts/routes
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
<nav>
|
|
197
|
+
{crumbs.map((c, i) => (
|
|
198
|
+
<span key={i}>
|
|
199
|
+
<a href={c.href}>{c.label}</a>
|
|
200
|
+
{i < crumbs.length - 1 && " > "}
|
|
201
|
+
</span>
|
|
202
|
+
))}
|
|
203
|
+
</nav>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// With selector
|
|
208
|
+
const lastCrumb = useHandle(Breadcrumbs, data => data.at(-1));
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Handles can be passed as props from server to client components:
|
|
212
|
+
|
|
213
|
+
```tsx
|
|
214
|
+
// Server component
|
|
215
|
+
path("/dashboard", (ctx) => {
|
|
216
|
+
const push = ctx.use(Breadcrumbs);
|
|
217
|
+
push({ label: "Dashboard", href: "/dashboard" });
|
|
218
|
+
return <DashboardNav handle={Breadcrumbs} />;
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
// Client component — typeof infers the full Handle<T> type
|
|
222
|
+
"use client";
|
|
223
|
+
import { useHandle } from "@rangojs/router/client";
|
|
224
|
+
import type { Breadcrumbs } from "../handles";
|
|
225
|
+
|
|
226
|
+
function DashboardNav({ handle }: { handle: typeof Breadcrumbs }) {
|
|
227
|
+
const crumbs = useHandle(handle);
|
|
228
|
+
return <nav>{crumbs.map(c => <a href={c.href}>{c.label}</a>)}</nav>;
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
RSC serialization strips the `collect` function via `toJSON()`. On the client,
|
|
233
|
+
`useHandle()` recovers it from the module-level registry (populated when
|
|
234
|
+
`createHandle()` runs during module initialization).
|
|
235
|
+
|
|
236
|
+
## Action Hooks
|
|
237
|
+
|
|
238
|
+
### useAction()
|
|
239
|
+
|
|
240
|
+
Track state of server action invocations:
|
|
241
|
+
|
|
242
|
+
```tsx
|
|
243
|
+
"use client";
|
|
244
|
+
import { useAction } from "@rangojs/router";
|
|
245
|
+
import { addToCart } from "../actions/cart";
|
|
246
|
+
|
|
247
|
+
function AddToCartButton({ productId }: { productId: string }) {
|
|
248
|
+
const { state, error, result } = useAction(addToCart);
|
|
249
|
+
|
|
250
|
+
// state: 'idle' | 'loading' | 'streaming'
|
|
251
|
+
// actionId: string | null
|
|
252
|
+
// payload: unknown | null (input data)
|
|
253
|
+
// error: Error | null
|
|
254
|
+
// result: unknown | null (return value)
|
|
255
|
+
|
|
256
|
+
return (
|
|
257
|
+
<form action={addToCart}>
|
|
258
|
+
<input type="hidden" name="productId" value={productId} />
|
|
259
|
+
<button disabled={state === 'loading'}>
|
|
260
|
+
{state === 'loading' ? 'Adding...' : 'Add to Cart'}
|
|
261
|
+
</button>
|
|
262
|
+
{error && <p className="error">{error.message}</p>}
|
|
263
|
+
</form>
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Match by string suffix (convenient but may be ambiguous)
|
|
268
|
+
const isLoading = useAction('addToCart', s => s.state === 'loading');
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## State Hooks
|
|
272
|
+
|
|
273
|
+
### useLocationState()
|
|
274
|
+
|
|
275
|
+
Read type-safe state from history:
|
|
276
|
+
|
|
277
|
+
```tsx
|
|
278
|
+
"use client";
|
|
279
|
+
import { useLocationState, createLocationState } from "@rangojs/router";
|
|
280
|
+
|
|
281
|
+
// Define typed state
|
|
282
|
+
export const ProductState = createLocationState<{
|
|
283
|
+
name: string;
|
|
284
|
+
price: number;
|
|
285
|
+
}>();
|
|
286
|
+
|
|
287
|
+
function ProductHeader() {
|
|
288
|
+
const state = useLocationState(ProductState);
|
|
289
|
+
// { name: string; price: number } | undefined
|
|
290
|
+
|
|
291
|
+
if (state) {
|
|
292
|
+
return <h1>{state.name} - ${state.price}</h1>;
|
|
293
|
+
}
|
|
294
|
+
return <h1>Loading...</h1>;
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Pass state through Link:
|
|
299
|
+
|
|
300
|
+
```tsx
|
|
301
|
+
import { Link } from "@rangojs/router/client";
|
|
302
|
+
import { ProductState } from "./state";
|
|
303
|
+
|
|
304
|
+
<Link
|
|
305
|
+
to="/product/123"
|
|
306
|
+
state={[ProductState({ name: "Widget", price: 99 })]}
|
|
307
|
+
>
|
|
308
|
+
View Product
|
|
309
|
+
</Link>
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
## Cache Hooks
|
|
313
|
+
|
|
314
|
+
### useClientCache()
|
|
315
|
+
|
|
316
|
+
Manually control client-side navigation cache:
|
|
317
|
+
|
|
318
|
+
```tsx
|
|
319
|
+
"use client";
|
|
320
|
+
import { useClientCache } from "@rangojs/router";
|
|
321
|
+
|
|
322
|
+
function SaveButton() {
|
|
323
|
+
const { clear } = useClientCache();
|
|
324
|
+
|
|
325
|
+
const handleSave = async () => {
|
|
326
|
+
await fetch('/api/data', {
|
|
327
|
+
method: 'POST',
|
|
328
|
+
body: JSON.stringify(data)
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// Invalidate cache after mutation
|
|
332
|
+
clear();
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
return <button onClick={handleSave}>Save</button>;
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
**Use cases**: REST API mutations, WebSocket updates, non-RSC data changes.
|
|
340
|
+
|
|
341
|
+
## Outlet Components
|
|
342
|
+
|
|
343
|
+
### Outlet / ParallelOutlet
|
|
344
|
+
|
|
345
|
+
Render child content in layouts:
|
|
346
|
+
|
|
347
|
+
```tsx
|
|
348
|
+
import { Outlet, ParallelOutlet } from "@rangojs/router";
|
|
349
|
+
|
|
350
|
+
function DashboardLayout({ children }: { children?: React.ReactNode }) {
|
|
351
|
+
return (
|
|
352
|
+
<div className="dashboard">
|
|
353
|
+
<aside>
|
|
354
|
+
<ParallelOutlet name="@sidebar" />
|
|
355
|
+
</aside>
|
|
356
|
+
<main>
|
|
357
|
+
{children ?? <Outlet />}
|
|
358
|
+
</main>
|
|
359
|
+
<ParallelOutlet name="@notifications" />
|
|
360
|
+
</div>
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### useOutlet()
|
|
366
|
+
|
|
367
|
+
Access outlet content programmatically:
|
|
368
|
+
|
|
369
|
+
```tsx
|
|
370
|
+
"use client";
|
|
371
|
+
import { useOutlet } from "@rangojs/router";
|
|
372
|
+
|
|
373
|
+
function ConditionalLayout() {
|
|
374
|
+
const outlet = useOutlet();
|
|
375
|
+
// ReactNode | null
|
|
376
|
+
|
|
377
|
+
return outlet ? (
|
|
378
|
+
<div className="with-content">{outlet}</div>
|
|
379
|
+
) : (
|
|
380
|
+
<div className="empty">No content</div>
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
## URL Hooks
|
|
386
|
+
|
|
387
|
+
### useHref()
|
|
388
|
+
|
|
389
|
+
Mount-aware href for client components inside `include()` scopes:
|
|
390
|
+
|
|
391
|
+
```tsx
|
|
392
|
+
"use client";
|
|
393
|
+
import { useHref, href, Link } from "@rangojs/router/client";
|
|
394
|
+
|
|
395
|
+
// Inside include("/shop", shopPatterns)
|
|
396
|
+
function ShopNav() {
|
|
397
|
+
const href = useHref();
|
|
398
|
+
|
|
399
|
+
return (
|
|
400
|
+
<>
|
|
401
|
+
{/* Local paths - auto-prefixed with /shop */}
|
|
402
|
+
<Link to={href("/cart")}>Cart</Link>
|
|
403
|
+
<Link to={href("/product/widget")}>Widget</Link>
|
|
404
|
+
</>
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
Use `useHref()` for local navigation. Use the bare `href()` function for absolute paths.
|
|
410
|
+
|
|
411
|
+
### useMount()
|
|
412
|
+
|
|
413
|
+
Returns the current `include()` mount path:
|
|
414
|
+
|
|
415
|
+
```tsx
|
|
416
|
+
"use client";
|
|
417
|
+
import { useMount } from "@rangojs/router/client";
|
|
418
|
+
|
|
419
|
+
function MountInfo() {
|
|
420
|
+
const mount = useMount(); // "/shop" inside include("/shop", ...)
|
|
421
|
+
return <span>Mounted at: {mount}</span>;
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
See `/links` for full URL generation guide including server-side `ctx.reverse`.
|
|
426
|
+
|
|
427
|
+
## Hook Summary
|
|
428
|
+
|
|
429
|
+
| Hook | Purpose | Returns |
|
|
430
|
+
|------|---------|---------|
|
|
431
|
+
| `useHref()` | Mount-aware href | `(path) => string` |
|
|
432
|
+
| `useMount()` | Current include() mount path | `string` |
|
|
433
|
+
| `useNavigation()` | Navigation state & control | state, navigate, refresh |
|
|
434
|
+
| `useSegments()` | URL path & segment IDs | path, segmentIds, location |
|
|
435
|
+
| `useLinkStatus()` | Link pending state | { pending } |
|
|
436
|
+
| `useLoader()` | Loader data (strict) | data, isLoading, error |
|
|
437
|
+
| `useFetchLoader()` | Loader with on-demand fetch | data, load, isLoading |
|
|
438
|
+
| `useLoaderData()` | All loader data | Record<string, any> |
|
|
439
|
+
| `useHandle()` | Accumulated handle data | T (handle type) |
|
|
440
|
+
| `useAction()` | Server action state | state, error, result |
|
|
441
|
+
| `useLocationState()` | History state | T \| undefined |
|
|
442
|
+
| `useClientCache()` | Cache control | { clear } |
|