@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,129 @@
|
|
|
1
|
+
# Cloudflare Workers: Streaming, Dev Tooling, and Deploy
|
|
2
|
+
|
|
3
|
+
## 10. Cloudflare Workers: streaming, dev tooling, and deploy
|
|
4
|
+
|
|
5
|
+
This is the part of an RR7-on-Cloudflare migration that the API tables above do
|
|
6
|
+
**not** cover, and it produces symptoms that look like router bugs but aren't.
|
|
7
|
+
RR7 serves a fully-rendered HTML/Turbo-Stream response; Rango serves a **streamed**
|
|
8
|
+
RSC/SSR response. Anything in the request path that was harmless for RR7 but
|
|
9
|
+
**buffers the response** silently kills that stream — the `loading()` /
|
|
10
|
+
`<Suspense>` fallback never shows and the page appears "awaited" (old content
|
|
11
|
+
held until everything resolves).
|
|
12
|
+
|
|
13
|
+
### 10a. Never buffer the Rango response in a custom worker entry
|
|
14
|
+
|
|
15
|
+
RR7 Cloudflare entries commonly **rewrite the response body** — `await
|
|
16
|
+
response.text()` / `await response.arrayBuffer()`, `HTMLRewriter`, cookie/URL
|
|
17
|
+
rewriting, hybrid-proxy bridging. That is fine for RR7's complete HTML, but if it
|
|
18
|
+
runs on the **Rango** path it consumes the stream and you lose streaming
|
|
19
|
+
entirely.
|
|
20
|
+
|
|
21
|
+
Rule: in the worker entry, the Rango response must pass through as a **stream**.
|
|
22
|
+
Buffer only on non-Rango branches (a legacy/SFRA proxy, an error page).
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
// BAD — buffers the whole RSC stream before returning (no streaming, no fallback)
|
|
26
|
+
const response = await router.fetch(request, { env, ctx });
|
|
27
|
+
const body = await response.text(); // <- drains the stream
|
|
28
|
+
return new Response(rewrite(body), response);
|
|
29
|
+
|
|
30
|
+
// GOOD — pass the body through as a stream (re-wrap headers only)
|
|
31
|
+
const response = await router.fetch(request, { env, ctx });
|
|
32
|
+
return new Response(response.body, response); // <- streams; safe to add headers
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
If you keep a legacy proxy (e.g. an SFRA/host fallback) that legitimately buffers
|
|
36
|
+
to rewrite HTML, gate it so it only runs for proxied paths — never for the
|
|
37
|
+
`router.fetch()` result.
|
|
38
|
+
|
|
39
|
+
Keep the custom entry itself (host/site resolution, sessions, proxy fallback) —
|
|
40
|
+
that's real product logic, not a deviation. Just don't let it touch the stream.
|
|
41
|
+
|
|
42
|
+
### 10b. Use `vite dev` / `vite preview`, not `wrangler dev`, for local work
|
|
43
|
+
|
|
44
|
+
The `@cloudflare/vite-plugin` runs **your worker entry** inside miniflare for both
|
|
45
|
+
`vite dev` and `vite preview`, with bindings + `.dev.vars`, and it **streams**.
|
|
46
|
+
`wrangler dev` does **not** stream RSC locally: it gzip-compresses the response by
|
|
47
|
+
buffering the entire body before sending. Measured on a bare streaming Worker
|
|
48
|
+
under `wrangler dev`: `Accept-Encoding: gzip` → first byte at the full delay
|
|
49
|
+
(buffered); `Accept-Encoding: identity` → first byte at ~3ms (streams). The
|
|
50
|
+
deployed Cloudflare edge does **streaming** compression, so this is a
|
|
51
|
+
**local-`wrangler dev`-only artifact** — do not mistake it for a production
|
|
52
|
+
streaming bug (verify the deployed edge with `curl` and watch chunk timing /
|
|
53
|
+
`transfer-encoding: chunked`).
|
|
54
|
+
|
|
55
|
+
```jsonc
|
|
56
|
+
// package.json — the standard rango-on-Workers shape
|
|
57
|
+
"dev": "vite", // streams, bindings, .dev.vars live
|
|
58
|
+
"build": "vite build",
|
|
59
|
+
"preview": "vite preview", // runs the built worker in workerd; also streams
|
|
60
|
+
// wrangler is only for: wrangler deploy / wrangler secret
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
If a workflow truly requires `wrangler dev`, declare `Content-Encoding: identity`
|
|
64
|
+
on streamed responses, gated to local hostnames (so the edge still compresses):
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
if (
|
|
68
|
+
isLocalHostname(url.hostname) &&
|
|
69
|
+
response.body &&
|
|
70
|
+
!response.headers.has("content-encoding")
|
|
71
|
+
) {
|
|
72
|
+
const ct = (response.headers.get("content-type") ?? "").toLowerCase();
|
|
73
|
+
if (ct.includes("text/html") || ct.includes("text/x-component")) {
|
|
74
|
+
const headers = new Headers(response.headers);
|
|
75
|
+
headers.set("content-encoding", "identity"); // wrangler skips gzip -> streams
|
|
76
|
+
return new Response(response.body, {
|
|
77
|
+
status: response.status,
|
|
78
|
+
statusText: response.statusText,
|
|
79
|
+
headers,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 10c. `.dev.vars` loads differently per tool
|
|
86
|
+
|
|
87
|
+
A frequent "my env vars vanished after migration" symptom — it's the tool, not
|
|
88
|
+
the file:
|
|
89
|
+
|
|
90
|
+
| Command | How `.dev.vars` is loaded |
|
|
91
|
+
| -------------- | ------------------------------------------------------------------------------------ |
|
|
92
|
+
| `wrangler dev` | Loaded natively at runtime (RR7's path — why it "worked before") |
|
|
93
|
+
| `vite dev` | Loaded **live** by the plugin (+ `.dev.vars.<CLOUDFLARE_ENV>` if a named env is set) |
|
|
94
|
+
| `vite preview` | Read from the **build output** `dist/<env>/.dev.vars`, written at `vite build` time |
|
|
95
|
+
|
|
96
|
+
So `vite preview` only sees vars that were present when you built — re-run
|
|
97
|
+
`vite build` after editing `.dev.vars`. (`vite dev` is the simplest: live vars +
|
|
98
|
+
streaming, no rebuild.)
|
|
99
|
+
|
|
100
|
+
### 10d. Build output moves from `build/` to `dist/`
|
|
101
|
+
|
|
102
|
+
RR7's `react-router build` writes to `build/`; Rango's `vite build` writes to
|
|
103
|
+
`dist/` (`dist/client`, `dist/rsc`, …). The leftover `build/` paths in
|
|
104
|
+
`wrangler.toml` and cleanup scripts must move, or you deploy stale/empty assets:
|
|
105
|
+
|
|
106
|
+
```toml
|
|
107
|
+
# wrangler.toml — was RR7's react-router build output
|
|
108
|
+
- assets = { directory = "./build/client/", binding = "ASSETS" }
|
|
109
|
+
+ assets = { directory = "./dist/client/", binding = "ASSETS" }
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
```jsonc
|
|
113
|
+
// package.json — clean the real output dir
|
|
114
|
+
- "cleanup": "rimraf ./build ./public/build"
|
|
115
|
+
+ "cleanup": "rimraf ./dist ./build ./public/build"
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### 10e. Deploy the built worker, not the source entry
|
|
119
|
+
|
|
120
|
+
`vite build` emits the **deployable** Worker config at `dist/<env>/wrangler.json`
|
|
121
|
+
with `main: "index.js"` (the bundled worker), `no_bundle: true`, and
|
|
122
|
+
`assets: "../client"`. A plain `wrangler deploy` run from the repo root uses the
|
|
123
|
+
**root** `wrangler.toml`, where `main` points at your **source** entry
|
|
124
|
+
(`app/.../worker.ts`) — and wrangler's bundler cannot resolve Rango's
|
|
125
|
+
`virtual:rsc-router/*` modules, so deploying the source entry fails or ships a
|
|
126
|
+
broken worker. Deploy the build output instead (e.g.
|
|
127
|
+
`wrangler deploy -c dist/<env>/wrangler.json`) or use the plugin's deploy flow.
|
|
128
|
+
RR7's `wrangler deploy` from root worked only because its worker was already
|
|
129
|
+
fully bundled by `react-router build`.
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# Middleware, Loading States, Navigation, Metadata, API Routes, and Theming
|
|
2
|
+
|
|
3
|
+
## 4. Middleware / Route Protection
|
|
4
|
+
|
|
5
|
+
React Router doesn't have built-in middleware. Protection is typically done in loaders:
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
// React Router: auth check in loader
|
|
9
|
+
export async function loader({ request }) {
|
|
10
|
+
const user = await getUser(request);
|
|
11
|
+
if (!user) throw redirect("/login");
|
|
12
|
+
return { user };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Rango: router.use() for request-level auth
|
|
16
|
+
const router = createRouter({})
|
|
17
|
+
.use(authInit) // all routes — resolves session
|
|
18
|
+
.use("/dashboard/*", requireAuth) // scoped guard
|
|
19
|
+
.routes(urlpatterns);
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Use `router.use()` for auth guards (wraps entire request including actions).
|
|
23
|
+
Use DSL `middleware()` for render-level concerns (context shaping, headers).
|
|
24
|
+
See `/middleware`.
|
|
25
|
+
|
|
26
|
+
## 5. Loading & Error States
|
|
27
|
+
|
|
28
|
+
### Loading / Suspense
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
// React Router: defer() + Suspense, or HydrateFallback
|
|
32
|
+
export async function loader() {
|
|
33
|
+
return defer({ data: fetchData() });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Rango: loading() DSL for automatic Suspense boundaries
|
|
37
|
+
path("/dashboard", DashboardPage, { name: "dashboard" }, () => [
|
|
38
|
+
loading(<DashboardSkeleton />),
|
|
39
|
+
])
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Error boundaries
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
// React Router:
|
|
46
|
+
{ path: "dashboard", element: <Dashboard />, errorElement: <ErrorPage /> }
|
|
47
|
+
|
|
48
|
+
// or with ErrorBoundary component:
|
|
49
|
+
function ErrorBoundary() {
|
|
50
|
+
const error = useRouteError();
|
|
51
|
+
return <div>Error: {error.message}</div>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Rango: errorBoundary() wrapping a group of routes
|
|
55
|
+
// Server-side error boundaries only receive `error` (no `reset` — server render
|
|
56
|
+
// cannot be retried; users can navigate away or refresh).
|
|
57
|
+
layout(<DashboardLayout />, () => [
|
|
58
|
+
errorBoundary(({ error }) => (
|
|
59
|
+
<div>
|
|
60
|
+
<h2>Something went wrong</h2>
|
|
61
|
+
<p>{error.message}</p>
|
|
62
|
+
</div>
|
|
63
|
+
)),
|
|
64
|
+
path("/dashboard", DashboardIndex, { name: "dashboard" }),
|
|
65
|
+
path("/dashboard/settings", Settings, { name: "settings" }),
|
|
66
|
+
])
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Not found
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
// React Router: { path: "*", element: <NotFound /> }
|
|
73
|
+
|
|
74
|
+
// Rango (app-level):
|
|
75
|
+
createRouter({
|
|
76
|
+
notFound: ({ pathname }) => <NotFoundPage pathname={pathname} />,
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
// Rango (route-level — catches notFound() thrown in handlers/loaders):
|
|
80
|
+
layout(<ShopLayout />, () => [
|
|
81
|
+
notFoundBoundary(<ProductNotFound />),
|
|
82
|
+
path("/product/:slug", ProductPage, { name: "product" }),
|
|
83
|
+
])
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## 6. Navigation
|
|
87
|
+
|
|
88
|
+
| React Router | Rango |
|
|
89
|
+
| ----------------------------------------- | -------------------------------------------------------------------------------- |
|
|
90
|
+
| `import { Link } from "react-router-dom"` | `import { Link } from "@rangojs/router/client"` |
|
|
91
|
+
| `<Link to="/about">` | `<Link to="/about">` |
|
|
92
|
+
| `useNavigate()` | `useRouter()` from `@rangojs/router/client` |
|
|
93
|
+
| `navigate("/about")` | `useRouter().push("/about")` |
|
|
94
|
+
| `navigate("/about", { replace: true })` | `useRouter().replace("/about")` |
|
|
95
|
+
| `navigate(-1)` | `useRouter().back()` |
|
|
96
|
+
| `useLocation().pathname` | `usePathname()` from `@rangojs/router/client` |
|
|
97
|
+
| `useSearchParams()` | `useSearchParams()` from `@rangojs/router/client` |
|
|
98
|
+
| `useParams()` | `useParams()` from `@rangojs/router/client` (or `ctx.params` in server handlers) |
|
|
99
|
+
| `useParams<T>()` | `useParams<T>()` — same generic annotation pattern |
|
|
100
|
+
| `<NavLink>` | `<Link>` with `usePathname()` for active state |
|
|
101
|
+
|
|
102
|
+
### useNavigate → useRouter
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
// React Router:
|
|
106
|
+
const navigate = useNavigate();
|
|
107
|
+
navigate("/dashboard");
|
|
108
|
+
navigate(-1);
|
|
109
|
+
|
|
110
|
+
// Rango:
|
|
111
|
+
const router = useRouter();
|
|
112
|
+
router.push("/dashboard");
|
|
113
|
+
router.back();
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## 7. Metadata / Head
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
// React Router: meta function export (framework mode)
|
|
120
|
+
export function meta() {
|
|
121
|
+
return [{ title: "Home" }, { name: "description", content: "Welcome" }];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Rango: Meta handle in server handlers
|
|
125
|
+
import { Meta } from "@rangojs/router";
|
|
126
|
+
|
|
127
|
+
const HomePage: Handler<"home"> = (ctx) => {
|
|
128
|
+
const meta = ctx.use(Meta);
|
|
129
|
+
meta({ title: "Home" });
|
|
130
|
+
meta({ name: "description", content: "Welcome" });
|
|
131
|
+
return <div>Home page</div>;
|
|
132
|
+
};
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Add `<MetaTags />` in the Document component's `<head>`:
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
import { MetaTags } from "@rangojs/router/client";
|
|
139
|
+
|
|
140
|
+
function Document({ children }: { children: ReactNode }) {
|
|
141
|
+
return (
|
|
142
|
+
<html>
|
|
143
|
+
<head>
|
|
144
|
+
<MetaTags />
|
|
145
|
+
</head>
|
|
146
|
+
<body>{children}</body>
|
|
147
|
+
</html>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## 8. API / Resource Routes
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
// React Router (framework mode):
|
|
156
|
+
// app/routes/api.users.ts
|
|
157
|
+
export async function loader() {
|
|
158
|
+
return Response.json(await getUsers());
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Rango: response routes
|
|
162
|
+
path.json(
|
|
163
|
+
"/api/users",
|
|
164
|
+
async () => {
|
|
165
|
+
return await getUsers();
|
|
166
|
+
},
|
|
167
|
+
{ name: "apiUsers" },
|
|
168
|
+
);
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
See `/response-routes` for `path.json()`, `path.text()`, `path.html()`, etc.
|
|
172
|
+
|
|
173
|
+
## 9. Theme / Dark Mode
|
|
174
|
+
|
|
175
|
+
Rango has a built-in theme system with FOUC prevention. If the React Router app
|
|
176
|
+
uses a custom theme provider or `next-themes`, replace it with Rango's theme API:
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
const router = createRouter({
|
|
180
|
+
theme: true, // or { defaultTheme: "system", attribute: "class" }
|
|
181
|
+
});
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Client components use `useTheme()` to read and toggle:
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
"use client";
|
|
188
|
+
import { useTheme } from "@rangojs/router/theme";
|
|
189
|
+
|
|
190
|
+
function ThemeToggle() {
|
|
191
|
+
const { theme, setTheme } = useTheme();
|
|
192
|
+
return <button onClick={() => setTheme(theme === "dark" ? "light" : "dark")}>{theme}</button>;
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
See `/theme` for full API including system detection and cookie persistence.
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# Data Fetching and Actions
|
|
2
|
+
|
|
3
|
+
## 3. Data Fetching
|
|
4
|
+
|
|
5
|
+
### Loaders → handler (the default migration)
|
|
6
|
+
|
|
7
|
+
In React Router, loaders and components are separate: the loader fetches data,
|
|
8
|
+
the component renders it via `useLoaderData()`. In Rango, server component
|
|
9
|
+
handlers do both — combine the loader and component into a single handler:
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
// React Router: separate loader + component
|
|
13
|
+
export async function loader({ params }) {
|
|
14
|
+
const product = await getProduct(params.slug);
|
|
15
|
+
return { product };
|
|
16
|
+
}
|
|
17
|
+
function ProductPage() {
|
|
18
|
+
const { product } = useLoaderData();
|
|
19
|
+
return <div>{product.name}</div>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Rango: handler fetches and renders directly
|
|
23
|
+
const ProductPage: Handler<"product"> = async (ctx) => {
|
|
24
|
+
const product = await getProduct(ctx.params.slug);
|
|
25
|
+
return <div>{product.name}</div>;
|
|
26
|
+
};
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
This is the standard migration path. The handler IS the loader — it fetches
|
|
30
|
+
data, then returns JSX. No separate data-fetching layer needed.
|
|
31
|
+
|
|
32
|
+
### When to use createLoader()
|
|
33
|
+
|
|
34
|
+
Rango's `createLoader()` is a live data layer, not a loader migration target.
|
|
35
|
+
Use it only when you need capabilities beyond what the handler provides:
|
|
36
|
+
|
|
37
|
+
- **Client-side reactive data** — `useLoader()` in client components for data
|
|
38
|
+
that updates without a full page navigation
|
|
39
|
+
- **Shared data across segments** — a loader registered on a layout is available
|
|
40
|
+
to all child routes via `ctx.use(Loader)` or `useLoader(Loader)`
|
|
41
|
+
- **Independent revalidation** — `revalidate()` on a specific loader after actions
|
|
42
|
+
- **Per-loader caching** — `loader(L, () => [cache({ ttl: 60 })])`
|
|
43
|
+
|
|
44
|
+
If the React Router loader just fetches data for its page component, merge it
|
|
45
|
+
into the handler. See `/loader` for when the live data layer is useful.
|
|
46
|
+
|
|
47
|
+
### Actions
|
|
48
|
+
|
|
49
|
+
React Router form actions map to Rango server actions:
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
// React Router:
|
|
53
|
+
export async function action({ request }) {
|
|
54
|
+
const formData = await request.formData();
|
|
55
|
+
await updateUser(formData.get("name"));
|
|
56
|
+
return redirect("/profile");
|
|
57
|
+
}
|
|
58
|
+
function EditProfile() {
|
|
59
|
+
return (
|
|
60
|
+
<Form method="post">
|
|
61
|
+
<input name="name" />
|
|
62
|
+
<button type="submit">Save</button>
|
|
63
|
+
</Form>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Rango: "use server" action + native form or useActionState
|
|
68
|
+
"use server";
|
|
69
|
+
import { redirect } from "@rangojs/router";
|
|
70
|
+
|
|
71
|
+
export async function updateProfile(formData: FormData): Promise<void> {
|
|
72
|
+
await updateUser(formData.get("name") as string);
|
|
73
|
+
throw redirect("/profile");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Client component:
|
|
77
|
+
function EditProfile() {
|
|
78
|
+
return (
|
|
79
|
+
<form action={updateProfile}>
|
|
80
|
+
<input name="name" />
|
|
81
|
+
<button type="submit">Save</button>
|
|
82
|
+
</form>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Key difference: React Router actions are route-scoped (declared per route).
|
|
88
|
+
Rango actions are function-scoped (`"use server"` on any exported async function).
|
|
89
|
+
|
|
90
|
+
### useLoaderData
|
|
91
|
+
|
|
92
|
+
There is no `useLoaderData()` in Rango. For most cases, the handler fetches
|
|
93
|
+
and renders directly (see above). When a client component needs live reactive
|
|
94
|
+
data, use `createLoader()` + `useLoader()`:
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
// React Router: useLoaderData() in client component
|
|
98
|
+
function ProductPrice() {
|
|
99
|
+
const { price } = useLoaderData();
|
|
100
|
+
return <span>{price}</span>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Rango: useLoader() reads from a registered loader (live data layer)
|
|
104
|
+
"use client";
|
|
105
|
+
import { useLoader } from "@rangojs/router/client";
|
|
106
|
+
import { PriceLoader } from "../loaders";
|
|
107
|
+
|
|
108
|
+
function ProductPrice() {
|
|
109
|
+
const { data } = useLoader(PriceLoader);
|
|
110
|
+
return <span>{data.price}</span>;
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`useLoader()` provides live data that stays fresh — it re-fetches on navigation
|
|
115
|
+
and after actions (controlled by `revalidate()`). This is different from
|
|
116
|
+
`useLoaderData()` which just reads a snapshot.
|
|
117
|
+
|
|
118
|
+
### useActionData
|
|
119
|
+
|
|
120
|
+
React Router's `useActionData()` reads the return value of a route-scoped
|
|
121
|
+
`action()`. In Rango, actions are standard React server actions (`"use server"`),
|
|
122
|
+
so all React patterns apply directly:
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
// React Router:
|
|
126
|
+
export async function action({ request }) {
|
|
127
|
+
const form = await request.formData();
|
|
128
|
+
const errors = validate(form);
|
|
129
|
+
if (errors) return { errors };
|
|
130
|
+
await save(form);
|
|
131
|
+
return { ok: true };
|
|
132
|
+
}
|
|
133
|
+
function EditForm() {
|
|
134
|
+
const data = useActionData();
|
|
135
|
+
return (
|
|
136
|
+
<Form method="post">
|
|
137
|
+
{data?.errors && <p>{data.errors}</p>}
|
|
138
|
+
<input name="title" />
|
|
139
|
+
<button>Save</button>
|
|
140
|
+
</Form>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Rango: useActionState (standard React hook)
|
|
145
|
+
"use client";
|
|
146
|
+
import { useActionState } from "react";
|
|
147
|
+
import { saveForm } from "../actions"; // "use server" function
|
|
148
|
+
|
|
149
|
+
function EditForm() {
|
|
150
|
+
const [state, action, pending] = useActionState(saveForm, null);
|
|
151
|
+
return (
|
|
152
|
+
<form action={action}>
|
|
153
|
+
{state?.errors && <p>{state.errors}</p>}
|
|
154
|
+
<input name="title" />
|
|
155
|
+
<button disabled={pending}>Save</button>
|
|
156
|
+
</form>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Since Rango uses RSC server actions, all React action patterns work:
|
|
162
|
+
`useActionState`, `useOptimistic`, `useTransition`, `startTransition`,
|
|
163
|
+
and plain `<form action={serverAction}>`. No framework-specific hook needed.
|
|
164
|
+
|
|
165
|
+
For the full guide — defining actions, validation with Zod, error handling,
|
|
166
|
+
revalidation rules, file uploads, and progressive enhancement — see
|
|
167
|
+
`/server-actions`.
|
|
168
|
+
|
|
169
|
+
### clientLoader / clientAction (framework mode)
|
|
170
|
+
|
|
171
|
+
RR7 framework mode's `clientLoader` and `clientAction` run in the browser.
|
|
172
|
+
Rango does not have a framework-level client loader/action concept — these
|
|
173
|
+
migrate to standard React client-side code:
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
// RR7: clientLoader fetching from a third-party API
|
|
177
|
+
export async function clientLoader() {
|
|
178
|
+
const res = await fetch("https://api.weather.com/current?city=london");
|
|
179
|
+
return res.json();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Rango: "use client" component with hooks
|
|
183
|
+
"use client";
|
|
184
|
+
import { useState, useEffect } from "react";
|
|
185
|
+
|
|
186
|
+
function WeatherWidget() {
|
|
187
|
+
const [weather, setWeather] = useState(null);
|
|
188
|
+
useEffect(() => {
|
|
189
|
+
fetch("https://api.weather.com/current?city=london")
|
|
190
|
+
.then((r) => r.json())
|
|
191
|
+
.then(setWeather);
|
|
192
|
+
}, []);
|
|
193
|
+
if (!weather) return <span>Loading...</span>;
|
|
194
|
+
return <span>{weather.temp}°C</span>;
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
The general rule: anything that ran in `clientLoader`/`clientAction` moves into
|
|
199
|
+
React hooks (`useState`, `useEffect`, `useActionState`, `useOptimistic`) inside
|
|
200
|
+
a `"use client"` component. There is no framework wrapper — it's just React.
|
|
201
|
+
|
|
202
|
+
### shouldRevalidate (framework mode)
|
|
203
|
+
|
|
204
|
+
RR7's `shouldRevalidate` export maps directly to Rango's `revalidate()` DSL:
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
// RR7:
|
|
208
|
+
export function shouldRevalidate({ actionResult, currentParams, nextParams }) {
|
|
209
|
+
if (actionResult) return true;
|
|
210
|
+
return currentParams.slug !== nextParams.slug;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Rango:
|
|
214
|
+
path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
215
|
+
revalidate(({ actionId, currentParams, nextParams }) => {
|
|
216
|
+
if (actionId) return true;
|
|
217
|
+
return currentParams.slug !== nextParams.slug;
|
|
218
|
+
}),
|
|
219
|
+
]);
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Note: RR7's `shouldRevalidate` controls client-side loader re-fetching. Rango's
|
|
223
|
+
`revalidate()` controls which segments re-run during partial rendering after
|
|
224
|
+
navigation or actions. The intent is the same — skip unnecessary work — but
|
|
225
|
+
the mechanism is segment-level rather than loader-level.
|