@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.
Files changed (222) hide show
  1. package/AGENTS.md +6 -10
  2. package/README.md +289 -938
  3. package/dist/bin/rango.js +271 -46
  4. package/dist/vite/index.js +673 -193
  5. package/package.json +10 -8
  6. package/skills/api-client/SKILL.md +1 -1
  7. package/skills/breadcrumbs/SKILL.md +31 -14
  8. package/skills/cache-guide/SKILL.md +5 -2
  9. package/skills/caching/SKILL.md +59 -4
  10. package/skills/catalog.json +271 -0
  11. package/skills/comparison/SKILL.md +50 -0
  12. package/skills/comparison/agents/openai.yaml +4 -0
  13. package/skills/comparison/references/framework-comparison.md +837 -0
  14. package/skills/composability/SKILL.md +83 -2
  15. package/skills/debug-manifest/SKILL.md +1 -1
  16. package/skills/defer-hydration/SKILL.md +235 -0
  17. package/skills/document-cache/SKILL.md +9 -1
  18. package/skills/fonts/SKILL.md +1 -1
  19. package/skills/handler-use/SKILL.md +8 -8
  20. package/skills/hooks/SKILL.md +54 -892
  21. package/skills/hooks/data.md +273 -0
  22. package/skills/hooks/handle-and-actions.md +103 -0
  23. package/skills/hooks/navigation.md +110 -0
  24. package/skills/hooks/outlets.md +41 -0
  25. package/skills/hooks/state.md +228 -0
  26. package/skills/hooks/urls.md +135 -0
  27. package/skills/host-router/SKILL.md +4 -4
  28. package/skills/i18n/SKILL.md +1 -1
  29. package/skills/intercept/SKILL.md +46 -14
  30. package/skills/layout/SKILL.md +27 -10
  31. package/skills/links/SKILL.md +1 -1
  32. package/skills/loader/SKILL.md +23 -1
  33. package/skills/middleware/SKILL.md +7 -3
  34. package/skills/migrate-nextjs/SKILL.md +167 -6
  35. package/skills/migrate-react-router/SKILL.md +59 -677
  36. package/skills/migrate-react-router/cloudflare-workers.md +129 -0
  37. package/skills/migrate-react-router/component-migration.md +196 -0
  38. package/skills/migrate-react-router/data-and-actions.md +225 -0
  39. package/skills/migrate-react-router/route-mapping.md +271 -0
  40. package/skills/mime-routes/SKILL.md +1 -1
  41. package/skills/observability/SKILL.md +9 -1
  42. package/skills/parallel/SKILL.md +23 -4
  43. package/skills/ppr/SKILL.md +622 -0
  44. package/skills/prerender/SKILL.md +28 -18
  45. package/skills/rango/SKILL.md +84 -25
  46. package/skills/response-routes/SKILL.md +15 -1
  47. package/skills/route/SKILL.md +71 -4
  48. package/skills/router-setup/SKILL.md +14 -3
  49. package/skills/scripts/SKILL.md +1 -1
  50. package/skills/server-actions/SKILL.md +3 -2
  51. package/skills/shell-manifest/SKILL.md +185 -0
  52. package/skills/streams-and-websockets/SKILL.md +1 -1
  53. package/skills/tailwind/SKILL.md +1 -1
  54. package/skills/testing/SKILL.md +2 -1
  55. package/skills/testing/handles.md +4 -2
  56. package/skills/testing/render-handler.md +15 -14
  57. package/skills/testing/reverse-and-types.md +8 -7
  58. package/skills/theme/SKILL.md +1 -1
  59. package/skills/typesafety/SKILL.md +45 -919
  60. package/skills/typesafety/env-and-bindings.md +254 -0
  61. package/skills/typesafety/generated-files-and-cli.md +335 -0
  62. package/skills/typesafety/params-and-search.md +153 -0
  63. package/skills/typesafety/route-types.md +209 -0
  64. package/skills/use-cache/SKILL.md +30 -3
  65. package/skills/vercel/SKILL.md +1 -1
  66. package/skills/view-transitions/SKILL.md +44 -1
  67. package/src/browser/event-controller.ts +62 -10
  68. package/src/browser/logging.ts +28 -0
  69. package/src/browser/merge-segment-loaders.ts +6 -4
  70. package/src/browser/navigation-bridge.ts +65 -16
  71. package/src/browser/navigation-client.ts +32 -2
  72. package/src/browser/navigation-store.ts +128 -14
  73. package/src/browser/network-error-handler.ts +34 -7
  74. package/src/browser/partial-update.ts +76 -17
  75. package/src/browser/prefetch/cache.ts +51 -11
  76. package/src/browser/prefetch/fetch.ts +59 -21
  77. package/src/browser/prefetch/queue.ts +19 -4
  78. package/src/browser/react/Link.tsx +13 -3
  79. package/src/browser/react/NavigationProvider.tsx +108 -4
  80. package/src/browser/response-adapter.ts +38 -9
  81. package/src/browser/rsc-router.tsx +54 -4
  82. package/src/browser/scroll-restoration.ts +7 -5
  83. package/src/browser/segment-reconciler.ts +31 -21
  84. package/src/browser/server-action-bridge.ts +22 -10
  85. package/src/browser/types.ts +54 -1
  86. package/src/build/generate-manifest.ts +155 -131
  87. package/src/build/index.ts +3 -1
  88. package/src/build/route-trie.ts +35 -7
  89. package/src/build/route-types/include-resolution.ts +347 -47
  90. package/src/build/runtime-discovery.ts +4 -1
  91. package/src/cache/cache-key-utils.ts +29 -0
  92. package/src/cache/cache-runtime.ts +262 -71
  93. package/src/cache/cache-scope.ts +2 -17
  94. package/src/cache/cache-tag.ts +60 -14
  95. package/src/cache/cf/cf-cache-store.ts +243 -20
  96. package/src/cache/document-cache.ts +54 -21
  97. package/src/cache/index.ts +1 -0
  98. package/src/cache/memory-segment-store.ts +110 -3
  99. package/src/cache/profile-registry.ts +15 -0
  100. package/src/cache/read-through-swr.ts +15 -1
  101. package/src/cache/segment-codec.ts +4 -4
  102. package/src/cache/shell-snapshot.ts +417 -0
  103. package/src/cache/types.ts +158 -0
  104. package/src/cache/vercel/vercel-cache-store.ts +401 -124
  105. package/src/client.rsc.tsx +0 -3
  106. package/src/client.tsx +0 -3
  107. package/src/cloudflare/tracing.ts +7 -8
  108. package/src/defer.ts +11 -22
  109. package/src/handle.ts +37 -15
  110. package/src/handles/MetaTags.tsx +16 -82
  111. package/src/handles/breadcrumbs.ts +12 -14
  112. package/src/handles/deferred-resolution.ts +127 -0
  113. package/src/handles/is-thenable.ts +7 -8
  114. package/src/handles/meta.ts +7 -44
  115. package/src/host/errors.ts +15 -0
  116. package/src/host/index.ts +1 -0
  117. package/src/index.rsc.ts +8 -2
  118. package/src/index.ts +19 -13
  119. package/src/internal-debug.ts +11 -8
  120. package/src/prerender.ts +17 -4
  121. package/src/redirect-origin.ts +14 -0
  122. package/src/render-error-thrower.tsx +20 -0
  123. package/src/route-content-wrapper.tsx +12 -5
  124. package/src/route-definition/dsl-helpers.ts +21 -32
  125. package/src/route-definition/helper-factories.ts +0 -2
  126. package/src/route-definition/helpers-types.ts +43 -43
  127. package/src/route-definition/index.ts +1 -2
  128. package/src/route-definition/resolve-handler-use.ts +0 -1
  129. package/src/route-definition/use-item-types.ts +3 -6
  130. package/src/route-map-builder.ts +41 -4
  131. package/src/route-types.ts +0 -5
  132. package/src/router/find-match.ts +86 -8
  133. package/src/router/instrument.ts +9 -4
  134. package/src/router/lazy-includes.ts +72 -12
  135. package/src/router/loader-resolution.ts +14 -2
  136. package/src/router/manifest.ts +56 -11
  137. package/src/router/match-api.ts +76 -32
  138. package/src/router/match-handlers.ts +181 -135
  139. package/src/router/match-middleware/background-revalidation.ts +40 -23
  140. package/src/router/match-middleware/cache-store.ts +39 -24
  141. package/src/router/match-result.ts +35 -15
  142. package/src/router/middleware.ts +64 -38
  143. package/src/router/navigation-snapshot.ts +7 -5
  144. package/src/router/parse-pattern.ts +115 -0
  145. package/src/router/pattern-matching.ts +53 -64
  146. package/src/router/prefetch-limits.ts +37 -0
  147. package/src/router/prerender-match.ts +11 -5
  148. package/src/router/preview-match.ts +3 -1
  149. package/src/router/request-classification.ts +23 -8
  150. package/src/router/route-snapshot.ts +14 -2
  151. package/src/router/router-context.ts +3 -1
  152. package/src/router/router-interfaces.ts +32 -1
  153. package/src/router/router-options.ts +30 -0
  154. package/src/router/segment-resolution/fresh.ts +39 -3
  155. package/src/router/segment-resolution/loader-cache.ts +93 -2
  156. package/src/router/segment-resolution/loader-mask.ts +60 -0
  157. package/src/router/segment-resolution/loader-snapshot.ts +259 -0
  158. package/src/router/segment-resolution/mask-nested.ts +83 -0
  159. package/src/router/segment-resolution/revalidation.ts +3 -0
  160. package/src/router/segment-resolution/view-transition-default.ts +35 -15
  161. package/src/router/substitute-pattern-params.ts +54 -35
  162. package/src/router/telemetry-otel.ts +6 -8
  163. package/src/router/telemetry.ts +9 -1
  164. package/src/router/tracing.ts +14 -5
  165. package/src/router/trie-matching.ts +19 -11
  166. package/src/router/url-params.ts +13 -0
  167. package/src/router.ts +47 -16
  168. package/src/rsc/full-payload.ts +70 -0
  169. package/src/rsc/handler.ts +60 -33
  170. package/src/rsc/manifest-init.ts +1 -1
  171. package/src/rsc/nonce.ts +10 -1
  172. package/src/rsc/progressive-enhancement.ts +61 -4
  173. package/src/rsc/redirect-guard.ts +2 -1
  174. package/src/rsc/rsc-rendering.ts +429 -37
  175. package/src/rsc/server-action.ts +25 -2
  176. package/src/rsc/shell-capture.ts +1190 -0
  177. package/src/rsc/shell-serve.ts +181 -0
  178. package/src/rsc/transition-gate.ts +89 -0
  179. package/src/rsc/types.ts +30 -0
  180. package/src/segment-loader-promise.ts +18 -0
  181. package/src/segment-system.tsx +149 -14
  182. package/src/server/context.ts +67 -9
  183. package/src/server/cookie-store.ts +73 -1
  184. package/src/server/loader-registry.ts +13 -1
  185. package/src/server/request-context.ts +169 -10
  186. package/src/ssr/index.tsx +462 -178
  187. package/src/ssr/inject-rsc-eager.ts +167 -0
  188. package/src/ssr/ssr-root.tsx +228 -0
  189. package/src/testing/collect-handle.ts +14 -8
  190. package/src/testing/dispatch.ts +152 -40
  191. package/src/testing/generated-routes.ts +27 -11
  192. package/src/testing/index.ts +6 -0
  193. package/src/testing/render-handler.ts +14 -0
  194. package/src/testing/render-route.tsx +13 -10
  195. package/src/testing/run-transition-when.ts +164 -0
  196. package/src/theme/ThemeProvider.tsx +36 -26
  197. package/src/types/handler-context.ts +1 -1
  198. package/src/types/index.ts +2 -0
  199. package/src/types/route-config.ts +19 -7
  200. package/src/types/segments.ts +100 -0
  201. package/src/urls/include-helper.ts +10 -8
  202. package/src/urls/include-provider.ts +71 -0
  203. package/src/urls/index.ts +1 -0
  204. package/src/urls/path-helper-types.ts +44 -12
  205. package/src/urls/path-helper.ts +5 -0
  206. package/src/urls/pattern-types.ts +36 -0
  207. package/src/urls/type-extraction.ts +43 -18
  208. package/src/urls/urls-function.ts +0 -1
  209. package/src/vercel/tracing.ts +7 -7
  210. package/src/vite/discovery/dev-prerender-cache.ts +117 -0
  211. package/src/vite/discovery/discover-routers.ts +1 -1
  212. package/src/vite/discovery/discovery-errors.ts +61 -0
  213. package/src/vite/index.ts +7 -0
  214. package/src/vite/inject-client-debug.ts +88 -0
  215. package/src/vite/plugins/vercel-output.ts +114 -25
  216. package/src/vite/plugins/version-injector.ts +22 -7
  217. package/src/vite/plugins/virtual-entries.ts +80 -22
  218. package/src/vite/rango.ts +29 -19
  219. package/src/vite/router-discovery.ts +171 -43
  220. package/src/vite/utils/prerender-utils.ts +17 -4
  221. package/src/vite/utils/shared-utils.ts +47 -0
  222. 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.