@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
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# Data Hooks
|
|
2
|
+
|
|
3
|
+
### useLoader()
|
|
4
|
+
|
|
5
|
+
Access loader data (strict - data guaranteed):
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
"use client";
|
|
9
|
+
import { useLoader } from "@rangojs/router/client";
|
|
10
|
+
import { ProductLoader } from "../loaders/product";
|
|
11
|
+
|
|
12
|
+
function ProductPrice() {
|
|
13
|
+
const { data, isLoading, error } = useLoader(ProductLoader);
|
|
14
|
+
|
|
15
|
+
// data: T (guaranteed - throws if not in context)
|
|
16
|
+
// isLoading: boolean
|
|
17
|
+
// error: Error | null
|
|
18
|
+
|
|
19
|
+
return <span>${data.price}</span>;
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**Precondition**: Loader must be registered on route via `loader()` helper.
|
|
24
|
+
|
|
25
|
+
Loaders can also be passed as props from server to client components:
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
"use client";
|
|
29
|
+
import { useLoader } from "@rangojs/router/client";
|
|
30
|
+
import type { ProductLoader } from "../loaders";
|
|
31
|
+
|
|
32
|
+
// typeof infers the full data type from the loader definition
|
|
33
|
+
function ProductCard({ loader }: { loader: typeof ProductLoader }) {
|
|
34
|
+
const { data } = useLoader(loader);
|
|
35
|
+
return <h2>{data.product.name}</h2>;
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### useFetchLoader()
|
|
40
|
+
|
|
41
|
+
Access loader with on-demand fetching (flexible):
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
"use client";
|
|
45
|
+
import { useFetchLoader } from "@rangojs/router/client";
|
|
46
|
+
import { SearchLoader } from "../loaders/search";
|
|
47
|
+
|
|
48
|
+
function SearchResults() {
|
|
49
|
+
const { data, load, isLoading, error } = useFetchLoader(SearchLoader);
|
|
50
|
+
|
|
51
|
+
// data: T | undefined (may be undefined if not fetched)
|
|
52
|
+
// load: (options?) => Promise<T>
|
|
53
|
+
// refetch: alias for load
|
|
54
|
+
|
|
55
|
+
const handleSearch = async (query: string) => {
|
|
56
|
+
await load({ params: { query } });
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div>
|
|
61
|
+
<input onChange={(e) => handleSearch(e.target.value)} />
|
|
62
|
+
{isLoading && <Spinner />}
|
|
63
|
+
{data?.results.map((r) => (
|
|
64
|
+
<Result key={r.id} {...r} />
|
|
65
|
+
))}
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Shared refetch behavior**:
|
|
72
|
+
|
|
73
|
+
When the loader is registered on the route via `loader()`, a plain
|
|
74
|
+
`load()` call (no options, or a trivially-defaulted GET with no
|
|
75
|
+
`params` and no `body`) broadcasts its result to every component
|
|
76
|
+
reading the same loader id. Layout, page, and parallel-slot reads
|
|
77
|
+
all converge on the new value:
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
// Layout button calls load() — the page read below sees the update too.
|
|
81
|
+
function Layout() {
|
|
82
|
+
const { data, load } = useLoader(CartLoader);
|
|
83
|
+
return <button onClick={() => load()}>Refresh ({data.count})</button>;
|
|
84
|
+
}
|
|
85
|
+
function Page() {
|
|
86
|
+
const { data } = useLoader(CartLoader); // updates with the layout's load()
|
|
87
|
+
return <span>{data.count} items</span>;
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`isLoading` and `error` follow the same scope. `throwOnError: true`
|
|
92
|
+
render-throws are scoped to the **originating** hook — sibling readers
|
|
93
|
+
see the error in their `error` state but their boundaries are not
|
|
94
|
+
triggered by someone else's failure. A successful follow-up `load()`
|
|
95
|
+
clears the shared error.
|
|
96
|
+
|
|
97
|
+
**`load()` calls that stay local** (no broadcast, per-hook state, same
|
|
98
|
+
semantics as the old per-component `useState`):
|
|
99
|
+
|
|
100
|
+
- `load({ params: { ... } })` — explicit params.
|
|
101
|
+
- `load({ method: "POST", body })` — mutations.
|
|
102
|
+
- Any `load()` on a `useFetchLoader(loader)` whose loader is **not**
|
|
103
|
+
registered on the current route. Two unrelated components calling
|
|
104
|
+
`load()` on the same fetchable-but-unregistered loader keep
|
|
105
|
+
independent results.
|
|
106
|
+
|
|
107
|
+
So the search/list pattern still works — two components calling
|
|
108
|
+
`load({ params: { q } })` with different `q` values each keep their
|
|
109
|
+
own result; they do not collapse to last-write-wins through a shared
|
|
110
|
+
store.
|
|
111
|
+
|
|
112
|
+
**Scoping refetch with a `key`**:
|
|
113
|
+
|
|
114
|
+
Pass a `key` to partition the shared refresh store. Only hooks using the
|
|
115
|
+
**same** `key` refresh together when one of them calls `load()`. This is a
|
|
116
|
+
client-side refresh identity only — it never changes the request sent to the
|
|
117
|
+
server, and is unrelated to the server `cache({ key })` option and to
|
|
118
|
+
`revalidate()`.
|
|
119
|
+
|
|
120
|
+
```tsx
|
|
121
|
+
// Two independent dashboards using the same loader. Without a key, one
|
|
122
|
+
// dashboard's load() would flip the other's spinner and value. With a key,
|
|
123
|
+
// they refresh independently.
|
|
124
|
+
function Dashboard({ id }: { id: string }) {
|
|
125
|
+
const { data, load } = useLoader(StatsLoader, { key: `dashboard:${id}` });
|
|
126
|
+
return <button onClick={() => load()}>Refresh {data.total}</button>;
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The `key` widens sharing in two ways the default cannot:
|
|
131
|
+
|
|
132
|
+
- **Parameterized GETs share.** `useFetchLoader(SearchLoader, { key: q })`
|
|
133
|
+
with the same `q` in two components share one result and refresh together —
|
|
134
|
+
a keyed `load({ params: { q } })` broadcasts to the group instead of staying
|
|
135
|
+
local. (Mutations — non-GET or `body` — stay local even with a key.)
|
|
136
|
+
- **Unregistered loaders share.** A `key` makes `useFetchLoader` of a loader
|
|
137
|
+
that is **not** registered on the route share too, letting unrelated
|
|
138
|
+
components opt into a common refresh group.
|
|
139
|
+
|
|
140
|
+
Lifecycle: a keyed read of an unregistered loader is reference-counted — its
|
|
141
|
+
shared value lives as long as at least one component using that key is mounted.
|
|
142
|
+
A persistent component (e.g. a header) keeps the value across navigations; a
|
|
143
|
+
route-scoped component's value is reclaimed when it unmounts. Registered-loader
|
|
144
|
+
reads (keyed or not) reset on navigation from fresh route data, as before.
|
|
145
|
+
|
|
146
|
+
**Refreshing multiple loaders together (`refreshGroup` + `useRefreshLoaders`)**:
|
|
147
|
+
|
|
148
|
+
`key` groups readers of one loader. To refresh **different** loaders together,
|
|
149
|
+
tag them with a shared `refreshGroup` name and trigger them with
|
|
150
|
+
`useRefreshLoaders()`. The hook takes no argument; you pass the group(s) to the
|
|
151
|
+
function it returns, so one `useRefreshLoaders()` can refresh different groups
|
|
152
|
+
depending on context. A read may carry **several** tags — pass an array — and is
|
|
153
|
+
refreshed when **any** of its groups is refreshed:
|
|
154
|
+
|
|
155
|
+
```tsx
|
|
156
|
+
function Profile() {
|
|
157
|
+
const { data } = useLoader(ProfileLoader, {
|
|
158
|
+
key: userId,
|
|
159
|
+
refreshGroup: "account",
|
|
160
|
+
});
|
|
161
|
+
return <span>{data.name}</span>;
|
|
162
|
+
}
|
|
163
|
+
function Orders() {
|
|
164
|
+
// Tagged into two groups: refreshed by "account" (the whole set) or the
|
|
165
|
+
// finer "orders" tag.
|
|
166
|
+
const { data } = useLoader(OrdersLoader, {
|
|
167
|
+
key: userId,
|
|
168
|
+
refreshGroup: ["account", "orders"],
|
|
169
|
+
});
|
|
170
|
+
return <span>{data.count} orders</span>;
|
|
171
|
+
}
|
|
172
|
+
function RefreshButtons() {
|
|
173
|
+
const refresh = useRefreshLoaders();
|
|
174
|
+
return (
|
|
175
|
+
<>
|
|
176
|
+
<button onClick={() => refresh("account")}>Refresh account</button>
|
|
177
|
+
<button onClick={() => refresh("orders")}>Refresh orders only</button>
|
|
178
|
+
<button onClick={() => refresh(["account", "orders"])}>
|
|
179
|
+
Refresh both
|
|
180
|
+
</button>
|
|
181
|
+
</>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
`refresh(groups)` accepts one name or an array and re-runs every currently-mounted
|
|
187
|
+
member tagged with **any** of them, with a **plain GET** against the current route
|
|
188
|
+
URL — no params, no body, no mutation methods, because a group spans loaders with
|
|
189
|
+
different shapes. A member that sits in two of the requested groups is fetched
|
|
190
|
+
once (members are unioned and deduped by read). It returns a promise that resolves
|
|
191
|
+
when all members settle and **rejects with an `AggregateError`** if any fail;
|
|
192
|
+
group refresh never render-throws, so handle failures at the await site
|
|
193
|
+
(`await refresh("account").catch(...)`). Each failing member also exposes its
|
|
194
|
+
error via its own read's `error`.
|
|
195
|
+
|
|
196
|
+
Multiple tags give you granular vs. whole-set refresh from one place: a coarse
|
|
197
|
+
tag (`"account"`) covers everything, while a finer tag (`"orders"`) targets a
|
|
198
|
+
subset. Sharing within a group is opt-in via `key`: members that share a `key`
|
|
199
|
+
share one value (and one fetch); a grouped reader **without** a `key` gets its own
|
|
200
|
+
private bucket, so a group refresh updates only that read and never leaks into
|
|
201
|
+
unrelated unkeyed reads of the same loader. A bucket may belong to several groups
|
|
202
|
+
at once (one read tagged with multiple names, or different reads tagging the same
|
|
203
|
+
keyed bucket with different names). Keep parameterized loaders on the single-loader
|
|
204
|
+
`key` — a plain-GET group refresh sends no params.
|
|
205
|
+
|
|
206
|
+
**Load options**:
|
|
207
|
+
|
|
208
|
+
```tsx
|
|
209
|
+
// JSON body — sent as application/json, available as ctx.body on the server
|
|
210
|
+
await load({
|
|
211
|
+
method: "POST",
|
|
212
|
+
params: { query: "test" },
|
|
213
|
+
body: { data: "value" },
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// FormData body — sent as multipart/form-data, available as ctx.formData on the server.
|
|
217
|
+
// Automatically detected: when body is a FormData instance, the request switches
|
|
218
|
+
// to multipart/form-data to preserve File objects and binary data.
|
|
219
|
+
const formData = new FormData();
|
|
220
|
+
formData.append("file", fileInput.files[0]);
|
|
221
|
+
await load({ method: "POST", body: formData });
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
**Body type auto-switching**: The `load()` function inspects the `body` value to
|
|
225
|
+
choose the encoding. If `body instanceof FormData`, the request is sent as
|
|
226
|
+
`multipart/form-data` (browser sets the boundary header automatically). Otherwise
|
|
227
|
+
the body is JSON-serialized and sent with `Content-Type: application/json`. On the
|
|
228
|
+
server, JSON bodies are available via `ctx.body` and FormData bodies via `ctx.formData`.
|
|
229
|
+
|
|
230
|
+
**File upload example**:
|
|
231
|
+
|
|
232
|
+
```tsx
|
|
233
|
+
"use client";
|
|
234
|
+
import { useFetchLoader } from "@rangojs/router/client";
|
|
235
|
+
import { FileUploadLoader } from "../loaders/upload";
|
|
236
|
+
|
|
237
|
+
function FileUploader() {
|
|
238
|
+
const { data, load, isLoading } = useFetchLoader(FileUploadLoader);
|
|
239
|
+
const formRef = useRef<HTMLFormElement>(null);
|
|
240
|
+
|
|
241
|
+
const handleSubmit = async (formData: FormData) => {
|
|
242
|
+
await load({ method: "POST", body: formData });
|
|
243
|
+
formRef.current?.reset();
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
return (
|
|
247
|
+
<form ref={formRef} action={handleSubmit}>
|
|
248
|
+
<input type="file" name="file" />
|
|
249
|
+
<button type="submit" disabled={isLoading}>
|
|
250
|
+
{isLoading ? "Uploading..." : "Upload"}
|
|
251
|
+
</button>
|
|
252
|
+
{data?.uploadedFile && <p>Uploaded: {data.uploadedFile.name}</p>}
|
|
253
|
+
</form>
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Server-side loader for the upload:
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
import { createLoader } from "@rangojs/router";
|
|
262
|
+
|
|
263
|
+
export const FileUploadLoader = createLoader(async (ctx) => {
|
|
264
|
+
"use server";
|
|
265
|
+
|
|
266
|
+
const file = ctx.formData?.get("file") as File | null;
|
|
267
|
+
if (file && file.size > 0) {
|
|
268
|
+
// Process file (save to R2, D1, etc.)
|
|
269
|
+
return { uploadedFile: { name: file.name, size: file.size } };
|
|
270
|
+
}
|
|
271
|
+
return { uploadedFile: null };
|
|
272
|
+
}, true); // true = fetchable (can be called from the client via load())
|
|
273
|
+
```
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Handle and Action Hooks
|
|
2
|
+
|
|
3
|
+
## Handle Hooks
|
|
4
|
+
|
|
5
|
+
### useHandle()
|
|
6
|
+
|
|
7
|
+
Access accumulated handle data from route segments:
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
"use client";
|
|
11
|
+
import { useHandle, Breadcrumbs } from "@rangojs/router/client";
|
|
12
|
+
|
|
13
|
+
function BreadcrumbNav() {
|
|
14
|
+
const crumbs = useHandle(Breadcrumbs);
|
|
15
|
+
// Array of { label, href } accumulated from layouts/routes
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<nav>
|
|
19
|
+
{crumbs.map((c, i) => (
|
|
20
|
+
<span key={i}>
|
|
21
|
+
<a href={c.href}>{c.label}</a>
|
|
22
|
+
{i < crumbs.length - 1 && " > "}
|
|
23
|
+
</span>
|
|
24
|
+
))}
|
|
25
|
+
</nav>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// With selector
|
|
30
|
+
const lastCrumb = useHandle(Breadcrumbs, (data) => data.at(-1));
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Handles can be passed as props from server to client components:
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
// Server component
|
|
37
|
+
path("/dashboard", (ctx) => {
|
|
38
|
+
const push = ctx.use(Breadcrumbs);
|
|
39
|
+
push({ label: "Dashboard", href: "/dashboard" });
|
|
40
|
+
return <DashboardNav handle={Breadcrumbs} />;
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
// Client component — typeof infers the full Handle<T> type
|
|
46
|
+
"use client";
|
|
47
|
+
import { useHandle, type Breadcrumbs } from "@rangojs/router/client";
|
|
48
|
+
|
|
49
|
+
function DashboardNav({ handle }: { handle: typeof Breadcrumbs }) {
|
|
50
|
+
const crumbs = useHandle(handle);
|
|
51
|
+
return (
|
|
52
|
+
<nav>
|
|
53
|
+
{crumbs.map((c) => (
|
|
54
|
+
<a href={c.href}>{c.label}</a>
|
|
55
|
+
))}
|
|
56
|
+
</nav>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
RSC serialization strips the `collect` function via `toJSON()`. On the client,
|
|
62
|
+
`useHandle()` recovers it from the module-level registry (populated when
|
|
63
|
+
`createHandle()` runs during module initialization).
|
|
64
|
+
|
|
65
|
+
## Action Hooks
|
|
66
|
+
|
|
67
|
+
For the full server-action guide (defining actions, `useActionState`,
|
|
68
|
+
`useOptimistic`, validation, revalidation, error handling, file uploads),
|
|
69
|
+
see `/server-actions`. `useAction()` below is a Rango-specific hook for
|
|
70
|
+
tracking actions called outside a `<form action={...}>` flow.
|
|
71
|
+
|
|
72
|
+
### useAction()
|
|
73
|
+
|
|
74
|
+
Track state of server action invocations:
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
"use client";
|
|
78
|
+
import { useAction } from "@rangojs/router/client";
|
|
79
|
+
import { addToCart } from "../actions/cart";
|
|
80
|
+
|
|
81
|
+
function AddToCartButton({ productId }: { productId: string }) {
|
|
82
|
+
const { state, error, result } = useAction(addToCart);
|
|
83
|
+
|
|
84
|
+
// state: 'idle' | 'loading' | 'streaming'
|
|
85
|
+
// actionId: string | null
|
|
86
|
+
// payload: unknown | null (input data)
|
|
87
|
+
// error: Error | null
|
|
88
|
+
// result: unknown | null (return value)
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<form action={addToCart}>
|
|
92
|
+
<input type="hidden" name="productId" value={productId} />
|
|
93
|
+
<button disabled={state === "loading"}>
|
|
94
|
+
{state === "loading" ? "Adding..." : "Add to Cart"}
|
|
95
|
+
</button>
|
|
96
|
+
{error && <p className="error">{error.message}</p>}
|
|
97
|
+
</form>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Match by string suffix (convenient but may be ambiguous)
|
|
102
|
+
const isLoading = useAction("addToCart", (s) => s.state === "loading");
|
|
103
|
+
```
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Navigation Hooks
|
|
2
|
+
|
|
3
|
+
### useNavigation()
|
|
4
|
+
|
|
5
|
+
Track reactive navigation state (state-only, no actions):
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
"use client";
|
|
9
|
+
import { useNavigation } from "@rangojs/router/client";
|
|
10
|
+
|
|
11
|
+
function NavIndicator() {
|
|
12
|
+
const nav = useNavigation();
|
|
13
|
+
|
|
14
|
+
// State properties
|
|
15
|
+
nav.state; // 'idle' | 'loading'
|
|
16
|
+
nav.isStreaming; // boolean
|
|
17
|
+
nav.location; // Current URL
|
|
18
|
+
nav.pendingUrl; // Target URL during navigation (or null)
|
|
19
|
+
|
|
20
|
+
return nav.state === "loading" ? <Spinner /> : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// With selector for performance (re-renders only when selected value changes)
|
|
24
|
+
function IsLoading() {
|
|
25
|
+
const isLoading = useNavigation((nav) => nav.state === "loading");
|
|
26
|
+
return isLoading ? <Spinner /> : null;
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### useRouter()
|
|
31
|
+
|
|
32
|
+
Access stable router actions (never causes re-renders):
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
"use client";
|
|
36
|
+
import { useRouter } from "@rangojs/router/client";
|
|
37
|
+
|
|
38
|
+
function NavigationControls() {
|
|
39
|
+
const router = useRouter();
|
|
40
|
+
|
|
41
|
+
router.push("/products"); // Navigate (adds history entry)
|
|
42
|
+
router.replace("/login"); // Navigate (replaces history entry)
|
|
43
|
+
router.refresh(); // Re-fetch current route data
|
|
44
|
+
router.prefetch("/dashboard"); // Prefetch for faster navigation
|
|
45
|
+
router.back(); // Go back in history
|
|
46
|
+
router.forward(); // Go forward in history
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
#### Skipping revalidation
|
|
51
|
+
|
|
52
|
+
Pass `revalidate: false` to skip the RSC server fetch for same-pathname navigations (search param or hash changes). The URL updates and all hooks re-render, but server components stay as-is.
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
// Update search params without server round-trip
|
|
56
|
+
router.push("/products?color=blue", { revalidate: false });
|
|
57
|
+
router.replace("/products?page=3", { revalidate: false });
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
If the pathname changes, `revalidate: false` is silently ignored and a full navigation occurs. This also works on `<Link>`:
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
<Link to="/products?color=blue" revalidate={false}>
|
|
64
|
+
Blue
|
|
65
|
+
</Link>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Plain `<a>` tags can opt in via `data-revalidate="false"`.
|
|
69
|
+
|
|
70
|
+
### useSegments()
|
|
71
|
+
|
|
72
|
+
Access current URL path and matched route segments:
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
"use client";
|
|
76
|
+
import { useSegments } from "@rangojs/router/client";
|
|
77
|
+
|
|
78
|
+
function Breadcrumbs() {
|
|
79
|
+
const { path, segmentIds, location } = useSegments();
|
|
80
|
+
|
|
81
|
+
// path: ["shop", "products", "123"] (split on "/", no leading slash on any element)
|
|
82
|
+
// segmentIds: ["L0", "L0L1", "L0L1R0"] (opaque internal short-codes, not route names)
|
|
83
|
+
// location: URL object
|
|
84
|
+
|
|
85
|
+
return <nav>{path.join(" > ")}</nav>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// With selector
|
|
89
|
+
const isShopRoute = useSegments((s) => s.path[0] === "shop");
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### useLinkStatus()
|
|
93
|
+
|
|
94
|
+
Track pending state inside a Link component:
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
"use client";
|
|
98
|
+
import { Link, useLinkStatus } from "@rangojs/router/client";
|
|
99
|
+
|
|
100
|
+
function LoadingIndicator() {
|
|
101
|
+
const { pending } = useLinkStatus();
|
|
102
|
+
return pending ? <Spinner /> : null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Must be inside Link
|
|
106
|
+
<Link to="/dashboard">
|
|
107
|
+
Dashboard
|
|
108
|
+
<LoadingIndicator />
|
|
109
|
+
</Link>;
|
|
110
|
+
```
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Outlet Components
|
|
2
|
+
|
|
3
|
+
### Outlet / ParallelOutlet
|
|
4
|
+
|
|
5
|
+
Render child content in layouts:
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { Outlet, ParallelOutlet } from "@rangojs/router/client";
|
|
9
|
+
|
|
10
|
+
function DashboardLayout({ children }: { children?: React.ReactNode }) {
|
|
11
|
+
return (
|
|
12
|
+
<div className="dashboard">
|
|
13
|
+
<aside>
|
|
14
|
+
<ParallelOutlet name="@sidebar" />
|
|
15
|
+
</aside>
|
|
16
|
+
<main>{children ?? <Outlet />}</main>
|
|
17
|
+
<ParallelOutlet name="@notifications" />
|
|
18
|
+
</div>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### useOutlet()
|
|
24
|
+
|
|
25
|
+
Access outlet content programmatically:
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
"use client";
|
|
29
|
+
import { useOutlet } from "@rangojs/router/client";
|
|
30
|
+
|
|
31
|
+
function ConditionalLayout() {
|
|
32
|
+
const outlet = useOutlet();
|
|
33
|
+
// ReactNode | null
|
|
34
|
+
|
|
35
|
+
return outlet ? (
|
|
36
|
+
<div className="with-content">{outlet}</div>
|
|
37
|
+
) : (
|
|
38
|
+
<div className="empty">No content</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
```
|