@rangojs/router 0.0.0-experimental.002d056c
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 +9 -0
- package/README.md +899 -0
- package/dist/bin/rango.js +1606 -0
- package/dist/vite/index.js +5153 -0
- package/package.json +177 -0
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/cache-guide/SKILL.md +262 -0
- package/skills/caching/SKILL.md +253 -0
- package/skills/composability/SKILL.md +172 -0
- package/skills/debug-manifest/SKILL.md +112 -0
- package/skills/document-cache/SKILL.md +182 -0
- package/skills/fonts/SKILL.md +167 -0
- package/skills/hooks/SKILL.md +704 -0
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +313 -0
- package/skills/layout/SKILL.md +310 -0
- package/skills/links/SKILL.md +239 -0
- package/skills/loader/SKILL.md +596 -0
- package/skills/middleware/SKILL.md +339 -0
- package/skills/mime-routes/SKILL.md +128 -0
- package/skills/parallel/SKILL.md +305 -0
- package/skills/prerender/SKILL.md +643 -0
- package/skills/rango/SKILL.md +118 -0
- package/skills/response-routes/SKILL.md +411 -0
- package/skills/route/SKILL.md +385 -0
- package/skills/router-setup/SKILL.md +439 -0
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +79 -0
- package/skills/typesafety/SKILL.md +623 -0
- package/skills/use-cache/SKILL.md +324 -0
- package/src/__internal.ts +273 -0
- package/src/bin/rango.ts +321 -0
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/action-response-classifier.ts +99 -0
- package/src/browser/event-controller.ts +899 -0
- package/src/browser/history-state.ts +80 -0
- package/src/browser/index.ts +18 -0
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/link-interceptor.ts +141 -0
- package/src/browser/logging.ts +55 -0
- package/src/browser/merge-segment-loaders.ts +134 -0
- package/src/browser/navigation-bridge.ts +638 -0
- package/src/browser/navigation-client.ts +261 -0
- package/src/browser/navigation-store.ts +806 -0
- package/src/browser/navigation-transaction.ts +297 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +582 -0
- package/src/browser/prefetch/cache.ts +206 -0
- package/src/browser/prefetch/fetch.ts +145 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +48 -0
- package/src/browser/prefetch/queue.ts +128 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +368 -0
- package/src/browser/react/NavigationProvider.tsx +413 -0
- package/src/browser/react/ScrollRestoration.tsx +94 -0
- package/src/browser/react/context.ts +59 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +52 -0
- package/src/browser/react/location-state-shared.ts +162 -0
- package/src/browser/react/location-state.ts +107 -0
- package/src/browser/react/mount-context.ts +37 -0
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/shallow-equal.ts +27 -0
- package/src/browser/react/use-action.ts +218 -0
- package/src/browser/react/use-client-cache.ts +58 -0
- package/src/browser/react/use-handle.ts +162 -0
- package/src/browser/react/use-href.tsx +40 -0
- package/src/browser/react/use-link-status.ts +135 -0
- package/src/browser/react/use-mount.ts +31 -0
- package/src/browser/react/use-navigation.ts +99 -0
- package/src/browser/react/use-params.ts +65 -0
- package/src/browser/react/use-pathname.ts +47 -0
- package/src/browser/react/use-router.ts +63 -0
- package/src/browser/react/use-search-params.ts +56 -0
- package/src/browser/react/use-segments.ts +171 -0
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +464 -0
- package/src/browser/scroll-restoration.ts +397 -0
- package/src/browser/segment-reconciler.ts +216 -0
- package/src/browser/segment-structure-assert.ts +83 -0
- package/src/browser/server-action-bridge.ts +667 -0
- package/src/browser/shallow.ts +40 -0
- package/src/browser/types.ts +547 -0
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +438 -0
- package/src/build/generate-route-types.ts +36 -0
- package/src/build/index.ts +35 -0
- package/src/build/route-trie.ts +265 -0
- package/src/build/route-types/ast-helpers.ts +25 -0
- package/src/build/route-types/ast-route-extraction.ts +98 -0
- package/src/build/route-types/codegen.ts +102 -0
- package/src/build/route-types/include-resolution.ts +411 -0
- package/src/build/route-types/param-extraction.ts +48 -0
- package/src/build/route-types/per-module-writer.ts +128 -0
- package/src/build/route-types/router-processing.ts +479 -0
- package/src/build/route-types/scan-filter.ts +78 -0
- package/src/build/runtime-discovery.ts +231 -0
- package/src/cache/background-task.ts +34 -0
- package/src/cache/cache-key-utils.ts +44 -0
- package/src/cache/cache-policy.ts +125 -0
- package/src/cache/cache-runtime.ts +338 -0
- package/src/cache/cache-scope.ts +382 -0
- package/src/cache/cf/cf-cache-store.ts +982 -0
- package/src/cache/cf/index.ts +29 -0
- package/src/cache/document-cache.ts +369 -0
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +44 -0
- package/src/cache/memory-segment-store.ts +328 -0
- package/src/cache/profile-registry.ts +73 -0
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +256 -0
- package/src/cache/taint.ts +98 -0
- package/src/cache/types.ts +342 -0
- package/src/client.rsc.tsx +85 -0
- package/src/client.tsx +601 -0
- package/src/component-utils.ts +76 -0
- package/src/components/DefaultDocument.tsx +27 -0
- package/src/context-var.ts +86 -0
- package/src/debug.ts +243 -0
- package/src/default-error-boundary.tsx +88 -0
- package/src/deps/browser.ts +8 -0
- package/src/deps/html-stream-client.ts +2 -0
- package/src/deps/html-stream-server.ts +2 -0
- package/src/deps/rsc.ts +10 -0
- package/src/deps/ssr.ts +2 -0
- package/src/errors.ts +365 -0
- package/src/handle.ts +135 -0
- package/src/handles/MetaTags.tsx +246 -0
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +7 -0
- package/src/handles/meta.ts +264 -0
- package/src/host/cookie-handler.ts +165 -0
- package/src/host/errors.ts +97 -0
- package/src/host/index.ts +53 -0
- package/src/host/pattern-matcher.ts +214 -0
- package/src/host/router.ts +352 -0
- package/src/host/testing.ts +79 -0
- package/src/host/types.ts +146 -0
- package/src/host/utils.ts +25 -0
- package/src/href-client.ts +222 -0
- package/src/index.rsc.ts +233 -0
- package/src/index.ts +277 -0
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +89 -0
- package/src/loader.ts +64 -0
- package/src/network-error-thrower.tsx +23 -0
- package/src/outlet-context.ts +15 -0
- package/src/outlet-provider.tsx +45 -0
- package/src/prerender/param-hash.ts +37 -0
- package/src/prerender/store.ts +185 -0
- package/src/prerender.ts +463 -0
- package/src/reverse.ts +330 -0
- package/src/root-error-boundary.tsx +289 -0
- package/src/route-content-wrapper.tsx +196 -0
- package/src/route-definition/dsl-helpers.ts +934 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +430 -0
- package/src/route-definition/index.ts +52 -0
- package/src/route-definition/redirect.ts +93 -0
- package/src/route-definition.ts +1 -0
- package/src/route-map-builder.ts +281 -0
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +259 -0
- package/src/router/content-negotiation.ts +116 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +287 -0
- package/src/router/find-match.ts +160 -0
- package/src/router/handler-context.ts +451 -0
- package/src/router/intercept-resolution.ts +397 -0
- package/src/router/lazy-includes.ts +236 -0
- package/src/router/loader-resolution.ts +420 -0
- package/src/router/logging.ts +251 -0
- package/src/router/manifest.ts +269 -0
- package/src/router/match-api.ts +620 -0
- package/src/router/match-context.ts +266 -0
- package/src/router/match-handlers.ts +440 -0
- package/src/router/match-middleware/background-revalidation.ts +223 -0
- package/src/router/match-middleware/cache-lookup.ts +634 -0
- package/src/router/match-middleware/cache-store.ts +295 -0
- package/src/router/match-middleware/index.ts +81 -0
- package/src/router/match-middleware/intercept-resolution.ts +306 -0
- package/src/router/match-middleware/segment-resolution.ts +193 -0
- package/src/router/match-pipelines.ts +179 -0
- package/src/router/match-result.ts +219 -0
- package/src/router/metrics.ts +282 -0
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +222 -0
- package/src/router/middleware.ts +749 -0
- package/src/router/pattern-matching.ts +563 -0
- package/src/router/prerender-match.ts +402 -0
- package/src/router/preview-match.ts +170 -0
- package/src/router/revalidation.ts +289 -0
- package/src/router/router-context.ts +320 -0
- package/src/router/router-interfaces.ts +452 -0
- package/src/router/router-options.ts +592 -0
- package/src/router/router-registry.ts +24 -0
- package/src/router/segment-resolution/fresh.ts +570 -0
- package/src/router/segment-resolution/helpers.ts +263 -0
- package/src/router/segment-resolution/loader-cache.ts +198 -0
- package/src/router/segment-resolution/revalidation.ts +1242 -0
- package/src/router/segment-resolution/static-store.ts +67 -0
- package/src/router/segment-resolution.ts +21 -0
- package/src/router/segment-wrappers.ts +291 -0
- package/src/router/telemetry-otel.ts +299 -0
- package/src/router/telemetry.ts +300 -0
- package/src/router/timeout.ts +148 -0
- package/src/router/trie-matching.ts +239 -0
- package/src/router/types.ts +170 -0
- package/src/router.ts +1006 -0
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +1089 -0
- package/src/rsc/helpers.ts +198 -0
- package/src/rsc/index.ts +36 -0
- package/src/rsc/loader-fetch.ts +209 -0
- package/src/rsc/manifest-init.ts +86 -0
- package/src/rsc/nonce.ts +32 -0
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +379 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +237 -0
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +348 -0
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +263 -0
- package/src/search-params.ts +230 -0
- package/src/segment-system.tsx +454 -0
- package/src/server/context.ts +591 -0
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +308 -0
- package/src/server/loader-registry.ts +133 -0
- package/src/server/request-context.ts +920 -0
- package/src/server/root-layout.tsx +10 -0
- package/src/server/tsconfig.json +14 -0
- package/src/server.ts +51 -0
- package/src/ssr/index.tsx +365 -0
- package/src/static-handler.ts +114 -0
- package/src/theme/ThemeProvider.tsx +297 -0
- package/src/theme/ThemeScript.tsx +61 -0
- package/src/theme/constants.ts +62 -0
- package/src/theme/index.ts +48 -0
- package/src/theme/theme-context.ts +44 -0
- package/src/theme/theme-script.ts +155 -0
- package/src/theme/types.ts +182 -0
- package/src/theme/use-theme.ts +44 -0
- package/src/types/boundaries.ts +158 -0
- package/src/types/cache-types.ts +198 -0
- package/src/types/error-types.ts +192 -0
- package/src/types/global-namespace.ts +100 -0
- package/src/types/handler-context.ts +687 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +183 -0
- package/src/types/route-config.ts +170 -0
- package/src/types/route-entry.ts +109 -0
- package/src/types/segments.ts +148 -0
- package/src/types.ts +1 -0
- package/src/urls/include-helper.ts +197 -0
- package/src/urls/index.ts +53 -0
- package/src/urls/path-helper-types.ts +339 -0
- package/src/urls/path-helper.ts +329 -0
- package/src/urls/pattern-types.ts +95 -0
- package/src/urls/response-types.ts +106 -0
- package/src/urls/type-extraction.ts +372 -0
- package/src/urls/urls-function.ts +98 -0
- package/src/urls.ts +1 -0
- package/src/use-loader.tsx +354 -0
- package/src/vite/discovery/bundle-postprocess.ts +184 -0
- package/src/vite/discovery/discover-routers.ts +344 -0
- package/src/vite/discovery/prerender-collection.ts +385 -0
- package/src/vite/discovery/route-types-writer.ts +258 -0
- package/src/vite/discovery/self-gen-tracking.ts +47 -0
- package/src/vite/discovery/state.ts +108 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +16 -0
- package/src/vite/plugin-types.ts +48 -0
- package/src/vite/plugins/cjs-to-esm.ts +93 -0
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/client-ref-hashing.ts +105 -0
- package/src/vite/plugins/expose-action-id.ts +363 -0
- package/src/vite/plugins/expose-id-utils.ts +287 -0
- package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
- package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
- package/src/vite/plugins/expose-ids/types.ts +45 -0
- package/src/vite/plugins/expose-internal-ids.ts +569 -0
- package/src/vite/plugins/refresh-cmd.ts +65 -0
- package/src/vite/plugins/use-cache-transform.ts +323 -0
- package/src/vite/plugins/version-injector.ts +83 -0
- package/src/vite/plugins/version-plugin.ts +266 -0
- package/src/vite/plugins/version.d.ts +12 -0
- package/src/vite/plugins/virtual-entries.ts +123 -0
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +445 -0
- package/src/vite/router-discovery.ts +777 -0
- package/src/vite/utils/ast-handler-extract.ts +517 -0
- package/src/vite/utils/banner.ts +36 -0
- package/src/vite/utils/bundle-analysis.ts +137 -0
- package/src/vite/utils/manifest-utils.ts +70 -0
- package/src/vite/utils/package-resolution.ts +121 -0
- package/src/vite/utils/prerender-utils.ts +189 -0
- package/src/vite/utils/shared-utils.ts +169 -0
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: loader
|
|
3
|
+
description: Define data loaders for fetching data in routes with createLoader
|
|
4
|
+
argument-hint: [loader]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Data Loaders with loader()
|
|
8
|
+
|
|
9
|
+
Loaders fetch data on the server and stream it to the client.
|
|
10
|
+
|
|
11
|
+
## Creating a Loader
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { createLoader } from "@rangojs/router";
|
|
15
|
+
|
|
16
|
+
export const ProductLoader = createLoader(async (ctx) => {
|
|
17
|
+
"use server";
|
|
18
|
+
|
|
19
|
+
const product = await ctx.env.DB.prepare(
|
|
20
|
+
"SELECT * FROM products WHERE slug = ?",
|
|
21
|
+
)
|
|
22
|
+
.bind(ctx.params.slug)
|
|
23
|
+
.first();
|
|
24
|
+
|
|
25
|
+
return { product };
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Supported export patterns
|
|
30
|
+
|
|
31
|
+
All of the following are equivalent and fully supported by the Vite transform:
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
// Direct export (most common)
|
|
35
|
+
export const ProductLoader = createLoader(handler);
|
|
36
|
+
|
|
37
|
+
// Separate declaration + named export
|
|
38
|
+
const ProductLoader = createLoader(handler);
|
|
39
|
+
export { ProductLoader };
|
|
40
|
+
|
|
41
|
+
// Aliased export
|
|
42
|
+
const InternalLoader = createLoader(handler);
|
|
43
|
+
export { InternalLoader as ProductLoader };
|
|
44
|
+
|
|
45
|
+
// Aliased import
|
|
46
|
+
import { createLoader as cl } from "@rangojs/router";
|
|
47
|
+
export const ProductLoader = cl(handler);
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The `export const` form and the `const + export { }` form both work for
|
|
51
|
+
client stubs, ID injection, and loader manifest tracking.
|
|
52
|
+
|
|
53
|
+
## Using Loaders in Routes
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import { urls } from "@rangojs/router";
|
|
57
|
+
import { ProductLoader } from "./loaders/product";
|
|
58
|
+
|
|
59
|
+
export const urlpatterns = urls(({ path, loader }) => [
|
|
60
|
+
path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
61
|
+
loader(ProductLoader),
|
|
62
|
+
]),
|
|
63
|
+
]);
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Consuming Loader Data
|
|
67
|
+
|
|
68
|
+
Loaders are the **live data layer** — they resolve fresh on every request.
|
|
69
|
+
The way you consume them depends on whether you're in a server component
|
|
70
|
+
(route handler) or a client component.
|
|
71
|
+
|
|
72
|
+
> **IMPORTANT: Prefer consuming loaders in client components.** Keeping data
|
|
73
|
+
> fetching in loaders and consumption in client components creates a clean
|
|
74
|
+
> separation: the server-side handler renders static markup that can be
|
|
75
|
+
> freely cached with `cache()`, while loader data stays fresh on every
|
|
76
|
+
> request. When you consume loaders in server handlers via `ctx.use()`, the
|
|
77
|
+
> handler output depends on the loader data, which means caching the handler
|
|
78
|
+
> also caches the data — defeating the purpose of the live data layer.
|
|
79
|
+
|
|
80
|
+
### In Client Components (Preferred)
|
|
81
|
+
|
|
82
|
+
Client components use `useLoader()` from `@rangojs/router/client`.
|
|
83
|
+
The loader **must** be registered with `loader()` in the route's DSL
|
|
84
|
+
segments so the framework knows to resolve it during SSR and stream
|
|
85
|
+
the data to the client:
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
"use client";
|
|
89
|
+
import { useLoader } from "@rangojs/router/client";
|
|
90
|
+
import { ProductLoader } from "./loaders/product";
|
|
91
|
+
|
|
92
|
+
function ProductDetails() {
|
|
93
|
+
const { data } = useLoader(ProductLoader);
|
|
94
|
+
return <div>{data.product.description}</div>;
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
// Route definition — loader() registration required for client consumption
|
|
100
|
+
path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
101
|
+
loader(ProductLoader), // Required for useLoader() in client components
|
|
102
|
+
]);
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### In Route Handlers (Server Components)
|
|
106
|
+
|
|
107
|
+
In server components, use `ctx.use(Loader)` directly in the route handler.
|
|
108
|
+
This doesn't require `loader()` registration in the DSL — it works
|
|
109
|
+
standalone. **However**, prefer client-side consumption when possible (see
|
|
110
|
+
note above).
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
import { ProductLoader } from "./loaders/product";
|
|
114
|
+
|
|
115
|
+
// Route handler — server component
|
|
116
|
+
path("/product/:slug", async (ctx) => {
|
|
117
|
+
const { product } = await ctx.use(ProductLoader);
|
|
118
|
+
return <h1>{product.name}</h1>;
|
|
119
|
+
}, { name: "product" })
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
When you do register with `loader()` in the DSL, `ctx.use()` returns the
|
|
123
|
+
same memoized result — loaders never run twice per request.
|
|
124
|
+
|
|
125
|
+
**Never use `useLoader()` in server components** — it is a client-only API.
|
|
126
|
+
|
|
127
|
+
### Summary
|
|
128
|
+
|
|
129
|
+
| Context | API | `loader()` DSL required? |
|
|
130
|
+
| ---------------------------- | ------------------- | ------------------------ |
|
|
131
|
+
| Client component (preferred) | `useLoader(Loader)` | **Yes** |
|
|
132
|
+
| Route handler (server) | `ctx.use(Loader)` | No |
|
|
133
|
+
|
|
134
|
+
## Loader Context
|
|
135
|
+
|
|
136
|
+
Loaders receive the same context as route handlers:
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
export const ProductLoader = createLoader(async (ctx) => {
|
|
140
|
+
"use server";
|
|
141
|
+
|
|
142
|
+
// URL params (may include client-provided overrides for fetchable loaders)
|
|
143
|
+
const { slug } = ctx.params;
|
|
144
|
+
|
|
145
|
+
// Server-trusted route params (from URL pattern matching, cannot be overridden)
|
|
146
|
+
const { slug: trustedSlug } = ctx.routeParams;
|
|
147
|
+
|
|
148
|
+
// Query params
|
|
149
|
+
const variant = ctx.url.searchParams.get("variant");
|
|
150
|
+
|
|
151
|
+
// Platform bindings (DB, KV, etc.) — plain bindings from createRouter<TEnv>()
|
|
152
|
+
const db = ctx.env.DB;
|
|
153
|
+
|
|
154
|
+
// Request headers
|
|
155
|
+
const auth = ctx.request.headers.get("Authorization");
|
|
156
|
+
|
|
157
|
+
// Variables set by middleware (from RSCRouter.Vars augmentation)
|
|
158
|
+
const user = ctx.get("user");
|
|
159
|
+
|
|
160
|
+
return { product: await fetchProduct(slug) };
|
|
161
|
+
});
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### params vs routeParams
|
|
165
|
+
|
|
166
|
+
- `ctx.params` — merged route params + explicit loader params. For fetchable
|
|
167
|
+
loaders called with `load(Loader, { params: { ... } })`, explicit params
|
|
168
|
+
override route-matched params.
|
|
169
|
+
- `ctx.routeParams` — server-trusted route params from URL pattern matching.
|
|
170
|
+
Cannot be overridden by client-provided params.
|
|
171
|
+
|
|
172
|
+
Use `ctx.routeParams` when you need trusted route identity for authorization
|
|
173
|
+
or resource scoping:
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
export const OrderLoader = createLoader(async (ctx) => {
|
|
177
|
+
"use server";
|
|
178
|
+
|
|
179
|
+
// Use routeParams for auth checks — client cannot spoof the URL-matched ID
|
|
180
|
+
const { orderId } = ctx.routeParams;
|
|
181
|
+
const user = ctx.get("user");
|
|
182
|
+
|
|
183
|
+
const order = await db.orders.get(orderId);
|
|
184
|
+
if (order.userId !== user.id)
|
|
185
|
+
throw new Response("Forbidden", { status: 403 });
|
|
186
|
+
|
|
187
|
+
return { order };
|
|
188
|
+
});
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Loader with Children
|
|
192
|
+
|
|
193
|
+
Add caching or revalidation to specific loaders:
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
197
|
+
// Cached loader
|
|
198
|
+
loader(ProductLoader, () => [cache({ ttl: 300 })]),
|
|
199
|
+
|
|
200
|
+
// Loader with revalidation control
|
|
201
|
+
loader(RelatedProductsLoader, () => [
|
|
202
|
+
revalidate(() => false), // Never revalidate
|
|
203
|
+
]),
|
|
204
|
+
|
|
205
|
+
// Loader that revalidates after cart actions
|
|
206
|
+
loader(CartLoader, () => [
|
|
207
|
+
revalidate(({ actionId }) => actionId?.includes("Cart") ?? false),
|
|
208
|
+
]),
|
|
209
|
+
]);
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Revalidation Contracts for Loader Dependencies
|
|
213
|
+
|
|
214
|
+
If a loader reads `ctx.get()` data produced by an outer handler/layout, share
|
|
215
|
+
the same named revalidation contract across producer and consumer segments.
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
// revalidation-contracts.ts
|
|
219
|
+
export const revalidateAccountScope = ({ actionId }) =>
|
|
220
|
+
actionId?.includes("src/actions/account.ts#") ?? false;
|
|
221
|
+
|
|
222
|
+
layout(AccountLayout, () => [
|
|
223
|
+
revalidate(revalidateAccountScope), // producer reruns
|
|
224
|
+
path("/account/orders", OrdersPage, { name: "account.orders" }, () => [
|
|
225
|
+
loader(OrdersLoader, () => [
|
|
226
|
+
revalidate(revalidateAccountScope), // consumer reruns
|
|
227
|
+
]),
|
|
228
|
+
]),
|
|
229
|
+
]);
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
For segments that depend on multiple upstream domains, compose multiple
|
|
233
|
+
contracts on both sides.
|
|
234
|
+
|
|
235
|
+
To keep loader route trees concise, export helper wrappers:
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
import { revalidate } from "@rangojs/router";
|
|
239
|
+
|
|
240
|
+
export const revalidateAccount = () => [revalidate(revalidateAccountScope)];
|
|
241
|
+
|
|
242
|
+
layout(AccountLayout, () => [
|
|
243
|
+
revalidateAccount(),
|
|
244
|
+
path("/account/orders", OrdersPage, { name: "account.orders" }, () => [
|
|
245
|
+
loader(OrdersLoader, () => [revalidateAccount()]),
|
|
246
|
+
]),
|
|
247
|
+
]);
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## Loaders: The Live Data Layer
|
|
251
|
+
|
|
252
|
+
Loaders are the live data layer of the router. They resolve fresh on every
|
|
253
|
+
request, even when the route's UI segments are served from cache. This is a
|
|
254
|
+
core design principle — route-level `cache()` caches rendered components but
|
|
255
|
+
never caches loader data. Loaders are excluded at storage time and re-resolved
|
|
256
|
+
on retrieval.
|
|
257
|
+
|
|
258
|
+
This means `cache()` gives you cached UI + fresh data by default. Pre-rendering
|
|
259
|
+
follows the same rule: at build time, loaders are skipped entirely (there is no
|
|
260
|
+
real request context), and at runtime the worker resolves them fresh against
|
|
261
|
+
the live database.
|
|
262
|
+
|
|
263
|
+
### Opting a Loader into Caching
|
|
264
|
+
|
|
265
|
+
To cache a specific loader's data, attach a `cache()` child:
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
loader(ProductLoader, () => [cache({ ttl: 300 })]),
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
The loader's data is cached independently from the route's segment cache,
|
|
272
|
+
using the same `SegmentCacheStore` (app-level or per-loader override).
|
|
273
|
+
|
|
274
|
+
Values are serialized through RSC Flight, so loaders can return ReactNode,
|
|
275
|
+
Promises, null, and any RSC-serializable type — all round-trip correctly
|
|
276
|
+
through the cache.
|
|
277
|
+
|
|
278
|
+
### Cache Key
|
|
279
|
+
|
|
280
|
+
The default cache key is `loader:{loaderId}:{pathname}:{sortedParams}`.
|
|
281
|
+
This can be customized at two levels:
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
// Full override — key function replaces the default entirely
|
|
285
|
+
loader(ProductLoader, () => [
|
|
286
|
+
cache({
|
|
287
|
+
ttl: 300,
|
|
288
|
+
key: (ctx) => `product:${ctx.params.slug}:${cookies().get("locale")?.value ?? "en"}`,
|
|
289
|
+
}),
|
|
290
|
+
]),
|
|
291
|
+
|
|
292
|
+
// Store-level keyGenerator — modifies the default key (e.g., adds a region prefix)
|
|
293
|
+
// Set in the store configuration, applies to all entries in that store
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Resolution priority (same as route-level `cache()`):
|
|
297
|
+
|
|
298
|
+
1. `key(ctx)` from cache options — full override
|
|
299
|
+
2. `store.keyGenerator(ctx, defaultKey)` — store-level modification
|
|
300
|
+
3. Default key — `loader:{id}:{pathname}:{params}`
|
|
301
|
+
|
|
302
|
+
If a custom key function throws, it falls back to the default key silently
|
|
303
|
+
(logged to console.error).
|
|
304
|
+
|
|
305
|
+
### Tags for Invalidation
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
// Static tags
|
|
309
|
+
loader(ProductLoader, () => [
|
|
310
|
+
cache({ ttl: 300, tags: ["products", "catalog"] }),
|
|
311
|
+
]),
|
|
312
|
+
|
|
313
|
+
// Dynamic tags
|
|
314
|
+
loader(ProductLoader, () => [
|
|
315
|
+
cache({
|
|
316
|
+
ttl: 300,
|
|
317
|
+
tags: (ctx) => [`product:${ctx.params.slug}`, "products"],
|
|
318
|
+
}),
|
|
319
|
+
]),
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Stale-While-Revalidate
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
loader(ProductLoader, () => [
|
|
326
|
+
cache({ ttl: 60, swr: 300 }),
|
|
327
|
+
]),
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
During the SWR window (60-360s), stale data is returned immediately while
|
|
331
|
+
fresh data is fetched in the background via `waitUntil`. After the SWR window
|
|
332
|
+
expires (360s+), the entry is treated as a cache miss.
|
|
333
|
+
|
|
334
|
+
### Conditional Caching
|
|
335
|
+
|
|
336
|
+
Skip the cache at runtime based on request properties:
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
loader(ProductLoader, () => [
|
|
340
|
+
cache({
|
|
341
|
+
ttl: 300,
|
|
342
|
+
condition: (ctx) => !ctx.request.headers.has("authorization"),
|
|
343
|
+
}),
|
|
344
|
+
]),
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
When `condition` returns false, the loader runs fresh and the cache is bypassed
|
|
348
|
+
entirely (no read, no write).
|
|
349
|
+
|
|
350
|
+
### Per-Loader Store Override
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
const hotStore = new MemorySegmentCacheStore({ defaults: { ttl: 10 } });
|
|
354
|
+
|
|
355
|
+
loader(PricingLoader, () => [
|
|
356
|
+
cache({ store: hotStore }),
|
|
357
|
+
]),
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Without an explicit store, the loader uses the app-level store from the
|
|
361
|
+
handler config (`cache.store`).
|
|
362
|
+
|
|
363
|
+
## Multiple Loaders
|
|
364
|
+
|
|
365
|
+
Routes can have multiple loaders that run in parallel:
|
|
366
|
+
|
|
367
|
+
```typescript
|
|
368
|
+
path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
369
|
+
loader(ProductLoader),
|
|
370
|
+
loader(RelatedProductsLoader),
|
|
371
|
+
loader(ReviewsLoader),
|
|
372
|
+
]);
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
## Layout Loaders
|
|
376
|
+
|
|
377
|
+
Loaders on layouts are shared by all child routes:
|
|
378
|
+
|
|
379
|
+
```typescript
|
|
380
|
+
layout(<ShopLayout />, () => [
|
|
381
|
+
// These loaders are available to all shop routes
|
|
382
|
+
loader(CartLoader),
|
|
383
|
+
loader(CategoriesLoader),
|
|
384
|
+
|
|
385
|
+
path("/shop", ShopIndex, { name: "index" }),
|
|
386
|
+
path("/shop/product/:slug", ProductPage, { name: "product" }),
|
|
387
|
+
])
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
## Passing Loaders as Props
|
|
391
|
+
|
|
392
|
+
Loaders can be passed as props from server to client components. RSC serialization
|
|
393
|
+
uses `toJSON()` to send only `{ __brand, $$id }` — the loader function is stripped.
|
|
394
|
+
|
|
395
|
+
```typescript
|
|
396
|
+
// Server component (route handler)
|
|
397
|
+
import { SlowLoader } from "../loaders";
|
|
398
|
+
|
|
399
|
+
path("/dashboard", () => <DashboardContent loader={SlowLoader} />, { name: "dashboard" }, () => [
|
|
400
|
+
loader(SlowLoader),
|
|
401
|
+
loading(<DashboardSkeleton />),
|
|
402
|
+
])
|
|
403
|
+
|
|
404
|
+
// Client component — use typeof for type-safe props
|
|
405
|
+
"use client";
|
|
406
|
+
import { useLoader } from "@rangojs/router/client";
|
|
407
|
+
import type { SlowLoader } from "../loaders";
|
|
408
|
+
|
|
409
|
+
function DashboardContent({ loader }: { loader: typeof SlowLoader }) {
|
|
410
|
+
const { data } = useLoader(loader);
|
|
411
|
+
return <div>{data.message}</div>;
|
|
412
|
+
}
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
Use `typeof MyLoader` for the prop type — it infers the full generic automatically.
|
|
416
|
+
|
|
417
|
+
## Streaming with Suspense
|
|
418
|
+
|
|
419
|
+
Loaders stream data. Use Suspense for loading states:
|
|
420
|
+
|
|
421
|
+
```typescript
|
|
422
|
+
// In route definition
|
|
423
|
+
path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
424
|
+
loader(ProductLoader),
|
|
425
|
+
loading(<ProductSkeleton />), // Shows while loader streams
|
|
426
|
+
])
|
|
427
|
+
|
|
428
|
+
// Or in component
|
|
429
|
+
function ProductPage() {
|
|
430
|
+
return (
|
|
431
|
+
<Suspense fallback={<ProductSkeleton />}>
|
|
432
|
+
<ProductDetails />
|
|
433
|
+
</Suspense>
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
## Fetchable Loaders
|
|
439
|
+
|
|
440
|
+
By default, loaders only run during SSR and navigation. Pass `true` as the second
|
|
441
|
+
argument to `createLoader` to make a loader **fetchable** — callable from the client
|
|
442
|
+
via `useFetchLoader()` and `load()`:
|
|
443
|
+
|
|
444
|
+
```typescript
|
|
445
|
+
import { createLoader } from "@rangojs/router";
|
|
446
|
+
|
|
447
|
+
export const SearchLoader = createLoader(async (ctx) => {
|
|
448
|
+
"use server";
|
|
449
|
+
|
|
450
|
+
const query = ctx.params.query ?? "";
|
|
451
|
+
const results = await ctx.env.DB.prepare(
|
|
452
|
+
"SELECT * FROM products WHERE name LIKE ?",
|
|
453
|
+
)
|
|
454
|
+
.bind(`%${query}%`)
|
|
455
|
+
.all();
|
|
456
|
+
|
|
457
|
+
return { results: results.results ?? [] };
|
|
458
|
+
}, true); // true = fetchable
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
### Fetchable Loader with Middleware
|
|
462
|
+
|
|
463
|
+
Pass an options object instead of `true` to attach per-loader middleware.
|
|
464
|
+
This middleware runs only on `_rsc_loader` fetch requests (client-side
|
|
465
|
+
`load()` / `useFetchLoader()` calls), not during SSR `ctx.use()` execution:
|
|
466
|
+
|
|
467
|
+
```typescript
|
|
468
|
+
import { createLoader } from "@rangojs/router";
|
|
469
|
+
import { authMiddleware } from "../middleware/auth";
|
|
470
|
+
import { rateLimitMiddleware } from "../middleware/rate-limit";
|
|
471
|
+
|
|
472
|
+
export const ProtectedLoader = createLoader(
|
|
473
|
+
async (ctx) => {
|
|
474
|
+
"use server";
|
|
475
|
+
|
|
476
|
+
const user = ctx.get("user");
|
|
477
|
+
return { orders: await db.orders.list(user.id) };
|
|
478
|
+
},
|
|
479
|
+
{ middleware: [authMiddleware, rateLimitMiddleware] },
|
|
480
|
+
);
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
The middleware uses the same `MiddlewareFn` signature as route/app middleware,
|
|
484
|
+
so you can reuse existing middleware functions directly.
|
|
485
|
+
|
|
486
|
+
Fetchable loaders support both GET and POST (PUT, PATCH, DELETE) from the client.
|
|
487
|
+
The `load()` function auto-detects the body type:
|
|
488
|
+
|
|
489
|
+
- **JSON body** (`body: { ... }`) — sent as `application/json`, available as `ctx.body`
|
|
490
|
+
- **FormData body** (`body: formData`) — sent as `multipart/form-data`, available as `ctx.formData`
|
|
491
|
+
|
|
492
|
+
### Mutation Context
|
|
493
|
+
|
|
494
|
+
When a fetchable loader receives a POST/PUT/PATCH/DELETE request, the context
|
|
495
|
+
includes additional fields depending on the body type:
|
|
496
|
+
|
|
497
|
+
```typescript
|
|
498
|
+
export const MutationLoader = createLoader(async (ctx) => {
|
|
499
|
+
"use server";
|
|
500
|
+
|
|
501
|
+
// JSON body — available as ctx.body (parsed object)
|
|
502
|
+
const data = ctx.body as { name: string; email: string };
|
|
503
|
+
|
|
504
|
+
// FormData body — available as ctx.formData
|
|
505
|
+
const file = ctx.formData?.get("file") as File | null;
|
|
506
|
+
const name = ctx.formData?.get("name") as string | null;
|
|
507
|
+
|
|
508
|
+
// Route params are always available
|
|
509
|
+
const { slug } = ctx.params;
|
|
510
|
+
|
|
511
|
+
return { success: true };
|
|
512
|
+
}, true);
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
### File Upload Example
|
|
516
|
+
|
|
517
|
+
```typescript
|
|
518
|
+
// loaders/upload.ts
|
|
519
|
+
import { createLoader } from "@rangojs/router";
|
|
520
|
+
|
|
521
|
+
export const FileUploadLoader = createLoader(async (ctx) => {
|
|
522
|
+
"use server";
|
|
523
|
+
|
|
524
|
+
const file = ctx.formData?.get("file") as File | null;
|
|
525
|
+
if (file && file.size > 0) {
|
|
526
|
+
// Save to R2, D1, etc.
|
|
527
|
+
await ctx.env.BUCKET.put(file.name, file.stream());
|
|
528
|
+
return { uploaded: { name: file.name, size: file.size, type: file.type } };
|
|
529
|
+
}
|
|
530
|
+
return { uploaded: null };
|
|
531
|
+
}, true);
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
Client usage — see `/hooks useFetchLoader` for the full client-side pattern.
|
|
535
|
+
|
|
536
|
+
## Complete Example
|
|
537
|
+
|
|
538
|
+
```typescript
|
|
539
|
+
// loaders/shop.ts
|
|
540
|
+
import { createLoader } from "@rangojs/router";
|
|
541
|
+
|
|
542
|
+
export const ProductLoader = createLoader(async (ctx) => {
|
|
543
|
+
"use server";
|
|
544
|
+
|
|
545
|
+
const product = await ctx.env.DB
|
|
546
|
+
.prepare("SELECT * FROM products WHERE slug = ?")
|
|
547
|
+
.bind(ctx.params.slug)
|
|
548
|
+
.first();
|
|
549
|
+
|
|
550
|
+
if (!product) {
|
|
551
|
+
throw new Response("Product not found", { status: 404 });
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return { product };
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
export const CartLoader = createLoader(async (ctx) => {
|
|
558
|
+
"use server";
|
|
559
|
+
|
|
560
|
+
const user = ctx.get("user");
|
|
561
|
+
if (!user) return { cart: null };
|
|
562
|
+
|
|
563
|
+
const cart = await ctx.env.KV.get(`cart:${user.id}`, "json");
|
|
564
|
+
return { cart };
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
// urls.tsx
|
|
568
|
+
export const urlpatterns = urls(({ path, layout, loader, loading, cache, revalidate }) => [
|
|
569
|
+
layout(<ShopLayout />, () => [
|
|
570
|
+
// Shared cart loader for all shop routes
|
|
571
|
+
loader(CartLoader, () => [
|
|
572
|
+
revalidate(({ actionId }) => actionId?.includes("Cart") ?? false),
|
|
573
|
+
]),
|
|
574
|
+
|
|
575
|
+
path("/shop/product/:slug", ProductPage, { name: "product" }, () => [
|
|
576
|
+
loader(ProductLoader, () => [cache({ ttl: 60 })]),
|
|
577
|
+
loading(<ProductSkeleton />),
|
|
578
|
+
]),
|
|
579
|
+
]),
|
|
580
|
+
]);
|
|
581
|
+
|
|
582
|
+
// pages/product.tsx — server component (route handler)
|
|
583
|
+
import { ProductLoader, CartLoader } from "./loaders/shop";
|
|
584
|
+
|
|
585
|
+
async function ProductPage(ctx) {
|
|
586
|
+
const { product } = await ctx.use(ProductLoader);
|
|
587
|
+
const { cart } = await ctx.use(CartLoader);
|
|
588
|
+
|
|
589
|
+
return (
|
|
590
|
+
<div>
|
|
591
|
+
<h1>{product.name}</h1>
|
|
592
|
+
<AddToCartButton productId={product.id} inCart={cart?.items.includes(product.id)} />
|
|
593
|
+
</div>
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
```
|