@rangojs/router 0.0.0-experimental.10 → 0.0.0-experimental.100
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 +1037 -4
- package/dist/bin/rango.js +1619 -157
- package/dist/vite/index.js +5762 -2301
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +71 -63
- package/skills/breadcrumbs/SKILL.md +252 -0
- package/skills/cache-guide/SKILL.md +294 -0
- package/skills/caching/SKILL.md +93 -23
- package/skills/composability/SKILL.md +172 -0
- package/skills/debug-manifest/SKILL.md +12 -8
- package/skills/document-cache/SKILL.md +18 -16
- package/skills/fonts/SKILL.md +6 -4
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +367 -71
- package/skills/host-router/SKILL.md +218 -0
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +176 -8
- package/skills/layout/SKILL.md +124 -3
- package/skills/links/SKILL.md +304 -25
- package/skills/loader/SKILL.md +474 -47
- package/skills/middleware/SKILL.md +207 -37
- package/skills/migrate-nextjs/SKILL.md +562 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/mime-routes/SKILL.md +15 -11
- package/skills/parallel/SKILL.md +272 -1
- package/skills/prerender/SKILL.md +467 -65
- package/skills/rango/SKILL.md +89 -21
- package/skills/response-routes/SKILL.md +152 -91
- package/skills/route/SKILL.md +305 -14
- package/skills/router-setup/SKILL.md +210 -32
- package/skills/server-actions/SKILL.md +739 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/theme/SKILL.md +9 -8
- package/skills/typesafety/SKILL.md +333 -86
- package/skills/use-cache/SKILL.md +324 -0
- package/skills/view-transitions/SKILL.md +212 -0
- package/src/__internal.ts +102 -4
- package/src/bin/rango.ts +312 -15
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/action-response-classifier.ts +99 -0
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +136 -68
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/link-interceptor.ts +24 -4
- package/src/browser/logging.ts +55 -0
- package/src/browser/merge-segment-loaders.ts +20 -12
- package/src/browser/navigation-bridge.ts +374 -561
- package/src/browser/navigation-client.ts +228 -70
- package/src/browser/navigation-store.ts +97 -55
- package/src/browser/navigation-transaction.ts +297 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +376 -315
- package/src/browser/prefetch/cache.ts +314 -0
- package/src/browser/prefetch/fetch.ts +282 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +48 -0
- package/src/browser/prefetch/queue.ts +191 -0
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +152 -0
- package/src/browser/react/Link.tsx +255 -71
- package/src/browser/react/NavigationProvider.tsx +152 -24
- package/src/browser/react/context.ts +11 -0
- package/src/browser/react/filter-segment-order.ts +55 -0
- package/src/browser/react/index.ts +15 -12
- package/src/browser/react/location-state-shared.ts +95 -53
- package/src/browser/react/location-state.ts +60 -15
- package/src/browser/react/mount-context.ts +6 -1
- 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 +29 -51
- package/src/browser/react/use-client-cache.ts +5 -3
- package/src/browser/react/use-handle.ts +30 -120
- package/src/browser/react/use-link-status.ts +6 -5
- package/src/browser/react/use-navigation.ts +44 -65
- package/src/browser/react/use-params.ts +78 -0
- package/src/browser/react/use-pathname.ts +47 -0
- package/src/browser/react/use-reverse.ts +99 -0
- package/src/browser/react/use-router.ts +83 -0
- package/src/browser/react/use-search-params.ts +56 -0
- package/src/browser/react/use-segments.ts +85 -99
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +246 -64
- package/src/browser/scroll-restoration.ts +127 -52
- package/src/browser/segment-reconciler.ts +243 -0
- package/src/browser/segment-structure-assert.ts +16 -0
- package/src/browser/server-action-bridge.ts +510 -603
- package/src/browser/shallow.ts +6 -1
- package/src/browser/types.ts +158 -48
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +84 -23
- package/src/build/generate-route-types.ts +39 -828
- package/src/build/index.ts +4 -5
- package/src/build/route-trie.ts +85 -32
- 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 +418 -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 +618 -0
- package/src/build/route-types/scan-filter.ts +85 -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 +342 -0
- package/src/cache/cache-scope.ts +167 -307
- package/src/cache/cf/cf-cache-store.ts +573 -21
- package/src/cache/cf/index.ts +13 -3
- package/src/cache/document-cache.ts +116 -77
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +1 -15
- package/src/cache/memory-segment-store.ts +191 -13
- 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 +153 -0
- package/src/cache/types.ts +72 -122
- package/src/client.rsc.tsx +6 -1
- package/src/client.tsx +118 -302
- package/src/component-utils.ts +4 -4
- package/src/components/DefaultDocument.tsx +5 -1
- package/src/context-var.ts +156 -0
- package/src/debug.ts +19 -9
- package/src/errors.ts +77 -7
- package/src/handle.ts +55 -10
- package/src/handles/MetaTags.tsx +73 -20
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +1 -0
- package/src/handles/meta.ts +30 -13
- package/src/host/cookie-handler.ts +21 -15
- package/src/host/errors.ts +8 -8
- package/src/host/index.ts +4 -7
- package/src/host/pattern-matcher.ts +27 -27
- package/src/host/router.ts +61 -39
- package/src/host/testing.ts +8 -8
- package/src/host/types.ts +15 -7
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +65 -45
- package/src/index.rsc.ts +138 -21
- package/src/index.ts +206 -51
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +25 -143
- package/src/loader.ts +27 -10
- package/src/network-error-thrower.tsx +3 -1
- package/src/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +45 -0
- package/src/prerender/param-hash.ts +4 -2
- package/src/prerender/store.ts +159 -13
- package/src/prerender.ts +397 -29
- package/src/response-utils.ts +28 -0
- package/src/reverse.ts +231 -121
- package/src/root-error-boundary.tsx +41 -29
- package/src/route-content-wrapper.tsx +7 -4
- package/src/route-definition/dsl-helpers.ts +1134 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +483 -0
- package/src/route-definition/index.ts +55 -0
- package/src/route-definition/redirect.ts +101 -0
- package/src/route-definition/resolve-handler-use.ts +155 -0
- package/src/route-definition.ts +1 -1431
- package/src/route-map-builder.ts +162 -123
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +66 -9
- package/src/router/content-negotiation.ts +215 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +9 -9
- package/src/router/find-match.ts +160 -0
- package/src/router/handler-context.ts +418 -86
- package/src/router/intercept-resolution.ts +35 -20
- package/src/router/lazy-includes.ts +237 -0
- package/src/router/loader-resolution.ts +359 -128
- package/src/router/logging.ts +251 -0
- package/src/router/manifest.ts +98 -32
- package/src/router/match-api.ts +196 -261
- package/src/router/match-context.ts +4 -2
- package/src/router/match-handlers.ts +441 -0
- package/src/router/match-middleware/background-revalidation.ts +108 -93
- package/src/router/match-middleware/cache-lookup.ts +415 -86
- package/src/router/match-middleware/cache-store.ts +91 -29
- package/src/router/match-middleware/intercept-resolution.ts +48 -21
- package/src/router/match-middleware/segment-resolution.ts +73 -9
- package/src/router/match-pipelines.ts +10 -45
- package/src/router/match-result.ts +154 -35
- package/src/router/metrics.ts +240 -15
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +209 -0
- package/src/router/middleware.ts +373 -371
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +292 -52
- package/src/router/prerender-match.ts +502 -0
- package/src/router/preview-match.ts +98 -0
- package/src/router/request-classification.ts +310 -0
- package/src/router/revalidation.ts +152 -39
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +41 -21
- package/src/router/router-interfaces.ts +484 -0
- package/src/router/router-options.ts +618 -0
- package/src/router/router-registry.ts +24 -0
- package/src/router/segment-resolution/fresh.ts +756 -0
- package/src/router/segment-resolution/helpers.ts +268 -0
- package/src/router/segment-resolution/loader-cache.ts +199 -0
- package/src/router/segment-resolution/revalidation.ts +1407 -0
- package/src/router/segment-resolution/static-store.ts +67 -0
- package/src/router/segment-resolution.ts +21 -1315
- package/src/router/segment-wrappers.ts +291 -0
- package/src/router/substitute-pattern-params.ts +56 -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 +111 -39
- package/src/router/types.ts +17 -9
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +642 -2011
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +864 -1114
- package/src/rsc/helpers.ts +181 -19
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +229 -0
- package/src/rsc/manifest-init.ts +90 -0
- package/src/rsc/nonce.ts +14 -0
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +395 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +360 -0
- package/src/rsc/rsc-rendering.ts +256 -0
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +360 -0
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +52 -11
- package/src/search-params.ts +230 -0
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +187 -38
- package/src/server/context.ts +333 -59
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +113 -15
- package/src/server/loader-registry.ts +24 -64
- package/src/server/request-context.ts +603 -109
- package/src/server.ts +35 -155
- package/src/ssr/index.tsx +107 -30
- package/src/static-handler.ts +126 -0
- package/src/theme/ThemeProvider.tsx +21 -15
- package/src/theme/ThemeScript.tsx +5 -5
- package/src/theme/constants.ts +5 -2
- package/src/theme/index.ts +4 -14
- package/src/theme/theme-context.ts +4 -30
- package/src/theme/theme-script.ts +21 -18
- 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 +764 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +209 -0
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-config.ts +170 -0
- package/src/types/route-entry.ts +120 -0
- package/src/types/segments.ts +167 -0
- package/src/types.ts +1 -1757
- package/src/urls/include-helper.ts +207 -0
- package/src/urls/index.ts +53 -0
- package/src/urls/path-helper-types.ts +372 -0
- package/src/urls/path-helper.ts +364 -0
- package/src/urls/pattern-types.ts +107 -0
- package/src/urls/response-types.ts +108 -0
- package/src/urls/type-extraction.ts +372 -0
- package/src/urls/urls-function.ts +98 -0
- package/src/urls.ts +1 -1282
- package/src/use-loader.tsx +161 -81
- package/src/vite/debug.ts +184 -0
- package/src/vite/discovery/bundle-postprocess.ts +181 -0
- package/src/vite/discovery/discover-routers.ts +376 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +486 -0
- package/src/vite/discovery/route-types-writer.ts +258 -0
- package/src/vite/discovery/self-gen-tracking.ts +73 -0
- package/src/vite/discovery/state.ts +117 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +15 -2063
- package/src/vite/plugin-types.ts +103 -0
- package/src/vite/plugins/cjs-to-esm.ts +98 -0
- package/src/vite/plugins/client-ref-dedup.ts +131 -0
- package/src/vite/plugins/client-ref-hashing.ts +117 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
- package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +107 -64
- package/src/vite/plugins/expose-id-utils.ts +299 -0
- package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +209 -0
- package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +127 -0
- package/src/vite/plugins/expose-ids/types.ts +45 -0
- package/src/vite/plugins/expose-internal-ids.ts +816 -0
- package/src/vite/plugins/performance-tracks.ts +96 -0
- package/src/vite/plugins/refresh-cmd.ts +127 -0
- package/src/vite/plugins/use-cache-transform.ts +336 -0
- package/src/vite/plugins/version-injector.ts +109 -0
- package/src/vite/plugins/version-plugin.ts +266 -0
- package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +497 -0
- package/src/vite/router-discovery.ts +1423 -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 +161 -0
- package/src/vite/utils/prerender-utils.ts +222 -0
- package/src/vite/utils/shared-utils.ts +170 -0
- package/CLAUDE.md +0 -43
- package/src/browser/lru-cache.ts +0 -69
- package/src/browser/request-controller.ts +0 -164
- package/src/cache/memory-store.ts +0 -253
- package/src/href-context.ts +0 -33
- package/src/router.gen.ts +0 -6
- package/src/urls.gen.ts +0 -8
- package/src/vite/expose-handle-id.ts +0 -209
- package/src/vite/expose-loader-id.ts +0 -426
- package/src/vite/expose-location-state-id.ts +0 -177
- package/src/vite/expose-prerender-handler-id.ts +0 -429
- package/src/vite/package-resolution.ts +0 -125
- /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
package/skills/loader/SKILL.md
CHANGED
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: loader
|
|
3
3
|
description: Define data loaders for fetching data in routes with createLoader
|
|
4
|
-
argument-hint: [
|
|
4
|
+
argument-hint: [loader]
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Data Loaders with loader()
|
|
8
8
|
|
|
9
|
-
Loaders fetch data on the server and stream it to the client.
|
|
9
|
+
Loaders fetch data on the server and stream it to the client. For mutations
|
|
10
|
+
(writes triggered by forms or buttons), use server actions instead — see
|
|
11
|
+
`/server-actions`. Loaders re-resolve after an action runs, so the typical
|
|
12
|
+
flow is _action mutates → loader re-reads → UI updates_.
|
|
10
13
|
|
|
11
14
|
## Creating a Loader
|
|
12
15
|
|
|
13
16
|
```typescript
|
|
14
17
|
import { createLoader } from "@rangojs/router";
|
|
15
18
|
|
|
16
|
-
export const ProductLoader = createLoader(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
export const ProductLoader = createLoader(async (ctx) => {
|
|
20
|
+
"use server";
|
|
21
|
+
|
|
22
|
+
const product = await ctx.env.DB.prepare(
|
|
23
|
+
"SELECT * FROM products WHERE slug = ?",
|
|
24
|
+
)
|
|
19
25
|
.bind(ctx.params.slug)
|
|
20
26
|
.first();
|
|
21
27
|
|
|
@@ -23,6 +29,30 @@ export const ProductLoader = createLoader("product", async (ctx) => {
|
|
|
23
29
|
});
|
|
24
30
|
```
|
|
25
31
|
|
|
32
|
+
### Supported export patterns
|
|
33
|
+
|
|
34
|
+
All of the following are equivalent and fully supported by the Vite transform:
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
// Direct export (most common)
|
|
38
|
+
export const ProductLoader = createLoader(handler);
|
|
39
|
+
|
|
40
|
+
// Separate declaration + named export
|
|
41
|
+
const ProductLoader = createLoader(handler);
|
|
42
|
+
export { ProductLoader };
|
|
43
|
+
|
|
44
|
+
// Aliased export
|
|
45
|
+
const InternalLoader = createLoader(handler);
|
|
46
|
+
export { InternalLoader as ProductLoader };
|
|
47
|
+
|
|
48
|
+
// Aliased import
|
|
49
|
+
import { createLoader as cl } from "@rangojs/router";
|
|
50
|
+
export const ProductLoader = cl(handler);
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The `export const` form and the `const + export { }` form both work for
|
|
54
|
+
client stubs, ID injection, and loader manifest tracking.
|
|
55
|
+
|
|
26
56
|
## Using Loaders in Routes
|
|
27
57
|
|
|
28
58
|
```typescript
|
|
@@ -38,53 +68,165 @@ export const urlpatterns = urls(({ path, loader }) => [
|
|
|
38
68
|
|
|
39
69
|
## Consuming Loader Data
|
|
40
70
|
|
|
41
|
-
|
|
71
|
+
Register loaders with `loader()` in the DSL and consume them in client
|
|
72
|
+
components with `useLoader()`. This is the recommended pattern — it keeps
|
|
73
|
+
data fetching on the server and consumption on the client, with a clean
|
|
74
|
+
separation that works correctly with `cache()`.
|
|
42
75
|
|
|
43
76
|
```typescript
|
|
44
|
-
|
|
77
|
+
"use client";
|
|
78
|
+
import { useLoader } from "@rangojs/router/client";
|
|
45
79
|
import { ProductLoader } from "./loaders/product";
|
|
46
80
|
|
|
47
|
-
|
|
48
|
-
const {
|
|
49
|
-
return <
|
|
81
|
+
function ProductDetails() {
|
|
82
|
+
const { data } = useLoader(ProductLoader);
|
|
83
|
+
return <div>{data.product.description}</div>;
|
|
50
84
|
}
|
|
51
85
|
```
|
|
52
86
|
|
|
53
|
-
### In Client Components
|
|
54
|
-
|
|
55
87
|
```typescript
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
88
|
+
// Route definition — loader() registration required
|
|
89
|
+
path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
90
|
+
loader(ProductLoader),
|
|
91
|
+
]);
|
|
92
|
+
```
|
|
59
93
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
94
|
+
DSL loaders are the **live data layer** — they resolve fresh on every
|
|
95
|
+
request, even when the route is inside a `cache()` boundary. The router
|
|
96
|
+
excludes them from the segment cache at storage time and re-resolves them
|
|
97
|
+
on retrieval. This means `cache()` gives you cached UI + fresh data by
|
|
98
|
+
default.
|
|
99
|
+
|
|
100
|
+
### Cache safety
|
|
101
|
+
|
|
102
|
+
DSL loaders can safely read `createVar({ cache: false })` variables
|
|
103
|
+
because they are always resolved fresh. The read guard is bypassed for
|
|
104
|
+
loader functions — they never produce stale data.
|
|
105
|
+
|
|
106
|
+
### ctx.use(Loader) — escape hatch
|
|
107
|
+
|
|
108
|
+
For cases where you need loader data in the server handler itself (e.g.,
|
|
109
|
+
to set ctx variables or make routing decisions), use `ctx.use(Loader)`:
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
path("/product/:slug", async (ctx) => {
|
|
113
|
+
const { product } = await ctx.use(ProductLoader);
|
|
114
|
+
ctx.set(Product, product); // make available to children
|
|
115
|
+
return <ProductPage />;
|
|
116
|
+
}, { name: "product" }, () => [
|
|
117
|
+
loader(ProductLoader), // still register for client consumption
|
|
118
|
+
])
|
|
64
119
|
```
|
|
65
120
|
|
|
121
|
+
When you register with `loader()` in the DSL, `ctx.use()` returns the
|
|
122
|
+
same memoized result — loaders never run twice per request.
|
|
123
|
+
|
|
124
|
+
**Limitations of ctx.use(Loader):**
|
|
125
|
+
|
|
126
|
+
- The handler output depends on the loader data. If the route is inside
|
|
127
|
+
`cache()`, the handler is cached with the loader result baked in —
|
|
128
|
+
defeating the live data guarantee.
|
|
129
|
+
- Non-cacheable variable reads (`createVar({ cache: false })`) inside the
|
|
130
|
+
handler still throw, even if the data came from a loader.
|
|
131
|
+
- Prefer DSL `loader()` + client `useLoader()` for data that depends on
|
|
132
|
+
non-cacheable context variables.
|
|
133
|
+
|
|
134
|
+
**Never use `useLoader()` in server components** — it is a client-only API.
|
|
135
|
+
|
|
136
|
+
### Summary
|
|
137
|
+
|
|
138
|
+
| Pattern | API | Cache-safe | Recommended |
|
|
139
|
+
| ---------------------- | ------------------- | ---------- | ----------- |
|
|
140
|
+
| DSL + client component | `useLoader(Loader)` | Yes | Yes |
|
|
141
|
+
| Handler escape hatch | `ctx.use(Loader)` | No | When needed |
|
|
142
|
+
|
|
66
143
|
## Loader Context
|
|
67
144
|
|
|
68
|
-
Loaders receive the same context as route handlers
|
|
145
|
+
Loaders receive the same context shape as route handlers.
|
|
146
|
+
|
|
147
|
+
### Full field surface
|
|
148
|
+
|
|
149
|
+
| Field | Type | Notes |
|
|
150
|
+
| -------------- | ------------------------------ | --------------------------------------------------------------------------------------------------- |
|
|
151
|
+
| `params` | `TParams` | Merged route + explicit loader params; overridable by fetchable `load({ params })`. |
|
|
152
|
+
| `routeParams` | `Record<string, string>` | Server-trusted route params from URL pattern matching; cannot be overridden. |
|
|
153
|
+
| `request` | `Request` | The incoming `Request` (headers, method, body, `signal` for abort). |
|
|
154
|
+
| `url` | `URL` | Parsed request URL. |
|
|
155
|
+
| `pathname` | `string` | URL pathname (shortcut for `ctx.url.pathname`). |
|
|
156
|
+
| `searchParams` | `URLSearchParams` | Shortcut for `ctx.url.searchParams`. |
|
|
157
|
+
| `search` | `ResolveSearchSchema<TSearch>` | Typed query params when a search schema is declared on the route; `{}` otherwise. |
|
|
158
|
+
| `env` | `TEnv` | Plain bindings from `createRouter<TEnv>()` (DB, KV, secrets, etc.). |
|
|
159
|
+
| `get` | `(key \| ContextVar) => value` | Reads variables/context-vars set by middleware. |
|
|
160
|
+
| `use` | `(loader \| handle) => T` | Access another loader's data (Promise) or a handle's collected data (after `await ctx.rendered()`). |
|
|
161
|
+
| `rendered` | `() => Promise<void>` | **Experimental.** DSL loaders only — waits for non-loader segments before reading handle data. |
|
|
162
|
+
| `method` | `string` | HTTP method. `"GET"` for SSR loader runs; reflects real method for fetchable loaders. |
|
|
163
|
+
| `body` | `TBody \| undefined` | Parsed request body for fetchable POST/PUT/PATCH/DELETE calls. |
|
|
164
|
+
| `formData` | `FormData \| undefined` | Present when a fetchable loader is invoked via form submission. |
|
|
165
|
+
| `reverse` | `ScopedReverseFunction` | Generate type-checked URLs from route names (same scoped semantics as route handlers). |
|
|
166
|
+
|
|
167
|
+
### Example
|
|
69
168
|
|
|
70
169
|
```typescript
|
|
71
|
-
export const ProductLoader = createLoader(
|
|
72
|
-
|
|
170
|
+
export const ProductLoader = createLoader(async (ctx) => {
|
|
171
|
+
"use server";
|
|
172
|
+
|
|
173
|
+
// URL params (may include client-provided overrides for fetchable loaders)
|
|
73
174
|
const { slug } = ctx.params;
|
|
74
175
|
|
|
176
|
+
// Server-trusted route params (from URL pattern matching, cannot be overridden)
|
|
177
|
+
const { slug: trustedSlug } = ctx.routeParams;
|
|
178
|
+
|
|
75
179
|
// Query params
|
|
76
180
|
const variant = ctx.url.searchParams.get("variant");
|
|
77
181
|
|
|
78
|
-
//
|
|
79
|
-
const db = ctx.env.
|
|
182
|
+
// Platform bindings (DB, KV, etc.) — plain bindings from createRouter<TEnv>()
|
|
183
|
+
const db = ctx.env.DB;
|
|
80
184
|
|
|
81
185
|
// Request headers
|
|
82
186
|
const auth = ctx.request.headers.get("Authorization");
|
|
83
187
|
|
|
84
|
-
// Variables set by middleware
|
|
85
|
-
const user = ctx.
|
|
188
|
+
// Variables set by middleware (from RSCRouter.Vars augmentation)
|
|
189
|
+
const user = ctx.get("user");
|
|
190
|
+
|
|
191
|
+
// Type-checked URLs for payloads. `.name` resolves within the current
|
|
192
|
+
// include() scope; a bare `name` resolves globally. See /route and
|
|
193
|
+
// /typesafety for scope rules and route-name autocomplete.
|
|
194
|
+
const detailUrl = ctx.reverse(".detail", { slug });
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
product: await fetchProduct(slug),
|
|
198
|
+
links: { self: detailUrl },
|
|
199
|
+
};
|
|
200
|
+
});
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
See `/route` for the full handler-context contract (shared with loaders) and
|
|
204
|
+
`/typesafety` for route-name typing that powers `ctx.reverse` autocomplete.
|
|
205
|
+
|
|
206
|
+
### params vs routeParams
|
|
207
|
+
|
|
208
|
+
- `ctx.params` — merged route params + explicit loader params. For fetchable
|
|
209
|
+
loaders called with `load(Loader, { params: { ... } })`, explicit params
|
|
210
|
+
override route-matched params.
|
|
211
|
+
- `ctx.routeParams` — server-trusted route params from URL pattern matching.
|
|
212
|
+
Cannot be overridden by client-provided params.
|
|
213
|
+
|
|
214
|
+
Use `ctx.routeParams` when you need trusted route identity for authorization
|
|
215
|
+
or resource scoping:
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
export const OrderLoader = createLoader(async (ctx) => {
|
|
219
|
+
"use server";
|
|
220
|
+
|
|
221
|
+
// Use routeParams for auth checks — client cannot spoof the URL-matched ID
|
|
222
|
+
const { orderId } = ctx.routeParams;
|
|
223
|
+
const user = ctx.get("user");
|
|
224
|
+
|
|
225
|
+
const order = await db.orders.get(orderId);
|
|
226
|
+
if (order.userId !== user.id)
|
|
227
|
+
throw new Response("Forbidden", { status: 403 });
|
|
86
228
|
|
|
87
|
-
return {
|
|
229
|
+
return { order };
|
|
88
230
|
});
|
|
89
231
|
```
|
|
90
232
|
|
|
@@ -95,22 +237,202 @@ Add caching or revalidation to specific loaders:
|
|
|
95
237
|
```typescript
|
|
96
238
|
path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
97
239
|
// Cached loader
|
|
98
|
-
loader(ProductLoader, () => [
|
|
99
|
-
cache({ ttl: 300 }),
|
|
100
|
-
]),
|
|
240
|
+
loader(ProductLoader, () => [cache({ ttl: 300 })]),
|
|
101
241
|
|
|
102
242
|
// Loader with revalidation control
|
|
103
243
|
loader(RelatedProductsLoader, () => [
|
|
104
|
-
revalidate(() => false),
|
|
244
|
+
revalidate(() => false), // Never revalidate
|
|
105
245
|
]),
|
|
106
246
|
|
|
107
247
|
// Loader that revalidates after cart actions
|
|
108
248
|
loader(CartLoader, () => [
|
|
109
249
|
revalidate(({ actionId }) => actionId?.includes("Cart") ?? false),
|
|
110
250
|
]),
|
|
111
|
-
])
|
|
251
|
+
]);
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### `revalidate()` return shapes
|
|
255
|
+
|
|
256
|
+
A `revalidate(fn)` callback can return one of four shapes. The chain
|
|
257
|
+
processes revalidators in order; each call's return controls how the
|
|
258
|
+
chain continues:
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
// 1) Hard decision — short-circuits the chain, used as the final answer.
|
|
262
|
+
revalidate(() => true);
|
|
263
|
+
revalidate(({ actionId }) => actionId?.includes("Cart") ?? false);
|
|
264
|
+
|
|
265
|
+
// 2) Soft decision — updates the running suggestion for downstream
|
|
266
|
+
// revalidators on the same segment, chain continues.
|
|
267
|
+
revalidate(({ defaultShouldRevalidate }) => ({
|
|
268
|
+
defaultShouldRevalidate: !defaultShouldRevalidate,
|
|
269
|
+
}));
|
|
270
|
+
|
|
271
|
+
// 3) Defer (no opinion) — leaves the running suggestion unchanged and
|
|
272
|
+
// continues to the next revalidator. Implicit return / null /
|
|
273
|
+
// undefined are all equivalent and consumer-friendly.
|
|
274
|
+
revalidate(({ actionId }) => {
|
|
275
|
+
if (actionId?.includes("Cart")) return true; // hard for this branch only
|
|
276
|
+
// implicit return — let downstream revalidators or the segment default decide
|
|
277
|
+
});
|
|
278
|
+
revalidate(() => undefined); // explicit defer
|
|
279
|
+
revalidate(() => null); // explicit defer
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
If every revalidator on a segment defers, the segment-type default
|
|
283
|
+
(e.g. params-changed for routes, `false` for parallels) is used.
|
|
284
|
+
|
|
285
|
+
### Revalidation Contracts for Loader Dependencies
|
|
286
|
+
|
|
287
|
+
If a loader reads `ctx.get()` data produced by an outer handler/layout, share
|
|
288
|
+
the same named revalidation contract across producer and consumer segments.
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
// revalidation-contracts.ts
|
|
292
|
+
export const revalidateAccountScope = ({ actionId }) =>
|
|
293
|
+
actionId?.includes("src/actions/account.ts#") ?? false;
|
|
294
|
+
|
|
295
|
+
layout(AccountLayout, () => [
|
|
296
|
+
revalidate(revalidateAccountScope), // producer reruns
|
|
297
|
+
path("/account/orders", OrdersPage, { name: "account.orders" }, () => [
|
|
298
|
+
loader(OrdersLoader, () => [
|
|
299
|
+
revalidate(revalidateAccountScope), // consumer reruns
|
|
300
|
+
]),
|
|
301
|
+
]),
|
|
302
|
+
]);
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
For segments that depend on multiple upstream domains, compose multiple
|
|
306
|
+
contracts on both sides.
|
|
307
|
+
|
|
308
|
+
To keep loader route trees concise, export helper wrappers:
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
import { revalidate } from "@rangojs/router";
|
|
312
|
+
|
|
313
|
+
export const revalidateAccount = () => [revalidate(revalidateAccountScope)];
|
|
314
|
+
|
|
315
|
+
layout(AccountLayout, () => [
|
|
316
|
+
revalidateAccount(),
|
|
317
|
+
path("/account/orders", OrdersPage, { name: "account.orders" }, () => [
|
|
318
|
+
loader(OrdersLoader, () => [revalidateAccount()]),
|
|
319
|
+
]),
|
|
320
|
+
]);
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
## Loaders: The Live Data Layer
|
|
324
|
+
|
|
325
|
+
Loaders are the live data layer of the router. They resolve fresh on every
|
|
326
|
+
request, even when the route's UI segments are served from cache. This is a
|
|
327
|
+
core design principle — route-level `cache()` caches rendered components but
|
|
328
|
+
never caches loader data. Loaders are excluded at storage time and re-resolved
|
|
329
|
+
on retrieval.
|
|
330
|
+
|
|
331
|
+
This means `cache()` gives you cached UI + fresh data by default. Pre-rendering
|
|
332
|
+
follows the same rule: at build time, loaders are skipped entirely (there is no
|
|
333
|
+
real request context), and at runtime the worker resolves them fresh against
|
|
334
|
+
the live database.
|
|
335
|
+
|
|
336
|
+
### Opting a Loader into Caching
|
|
337
|
+
|
|
338
|
+
To cache a specific loader's data, attach a `cache()` child:
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
loader(ProductLoader, () => [cache({ ttl: 300 })]),
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
The loader's data is cached independently from the route's segment cache,
|
|
345
|
+
using the same `SegmentCacheStore` (app-level or per-loader override).
|
|
346
|
+
|
|
347
|
+
Values are serialized through RSC Flight, so loaders can return ReactNode,
|
|
348
|
+
Promises, null, and any RSC-serializable type — all round-trip correctly
|
|
349
|
+
through the cache.
|
|
350
|
+
|
|
351
|
+
### Cache Key
|
|
352
|
+
|
|
353
|
+
The default cache key is `loader:{loaderId}:{pathname}:{sortedParams}`.
|
|
354
|
+
This can be customized at two levels:
|
|
355
|
+
|
|
356
|
+
```typescript
|
|
357
|
+
// Full override — key function replaces the default entirely
|
|
358
|
+
loader(ProductLoader, () => [
|
|
359
|
+
cache({
|
|
360
|
+
ttl: 300,
|
|
361
|
+
key: (ctx) => `product:${ctx.params.slug}:${cookies().get("locale")?.value ?? "en"}`,
|
|
362
|
+
}),
|
|
363
|
+
]),
|
|
364
|
+
|
|
365
|
+
// Store-level keyGenerator — modifies the default key (e.g., adds a region prefix)
|
|
366
|
+
// Set in the store configuration, applies to all entries in that store
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
Resolution priority (same as route-level `cache()`):
|
|
370
|
+
|
|
371
|
+
1. `key(ctx)` from cache options — full override
|
|
372
|
+
2. `store.keyGenerator(ctx, defaultKey)` — store-level modification
|
|
373
|
+
3. Default key — `loader:{id}:{pathname}:{params}`
|
|
374
|
+
|
|
375
|
+
If a custom key function throws, it falls back to the default key silently
|
|
376
|
+
(logged to console.error).
|
|
377
|
+
|
|
378
|
+
### Tags for Invalidation
|
|
379
|
+
|
|
380
|
+
```typescript
|
|
381
|
+
// Static tags
|
|
382
|
+
loader(ProductLoader, () => [
|
|
383
|
+
cache({ ttl: 300, tags: ["products", "catalog"] }),
|
|
384
|
+
]),
|
|
385
|
+
|
|
386
|
+
// Dynamic tags
|
|
387
|
+
loader(ProductLoader, () => [
|
|
388
|
+
cache({
|
|
389
|
+
ttl: 300,
|
|
390
|
+
tags: (ctx) => [`product:${ctx.params.slug}`, "products"],
|
|
391
|
+
}),
|
|
392
|
+
]),
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### Stale-While-Revalidate
|
|
396
|
+
|
|
397
|
+
```typescript
|
|
398
|
+
loader(ProductLoader, () => [
|
|
399
|
+
cache({ ttl: 60, swr: 300 }),
|
|
400
|
+
]),
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
During the SWR window (60-360s), stale data is returned immediately while
|
|
404
|
+
fresh data is fetched in the background via `waitUntil`. After the SWR window
|
|
405
|
+
expires (360s+), the entry is treated as a cache miss.
|
|
406
|
+
|
|
407
|
+
### Conditional Caching
|
|
408
|
+
|
|
409
|
+
Skip the cache at runtime based on request properties:
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
loader(ProductLoader, () => [
|
|
413
|
+
cache({
|
|
414
|
+
ttl: 300,
|
|
415
|
+
condition: (ctx) => !ctx.request.headers.has("authorization"),
|
|
416
|
+
}),
|
|
417
|
+
]),
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
When `condition` returns false, the loader runs fresh and the cache is bypassed
|
|
421
|
+
entirely (no read, no write).
|
|
422
|
+
|
|
423
|
+
### Per-Loader Store Override
|
|
424
|
+
|
|
425
|
+
```typescript
|
|
426
|
+
const hotStore = new MemorySegmentCacheStore({ defaults: { ttl: 10 } });
|
|
427
|
+
|
|
428
|
+
loader(PricingLoader, () => [
|
|
429
|
+
cache({ store: hotStore }),
|
|
430
|
+
]),
|
|
112
431
|
```
|
|
113
432
|
|
|
433
|
+
Without an explicit store, the loader uses the app-level store from the
|
|
434
|
+
handler config (`cache.store`).
|
|
435
|
+
|
|
114
436
|
## Multiple Loaders
|
|
115
437
|
|
|
116
438
|
Routes can have multiple loaders that run in parallel:
|
|
@@ -120,7 +442,7 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
|
120
442
|
loader(ProductLoader),
|
|
121
443
|
loader(RelatedProductsLoader),
|
|
122
444
|
loader(ReviewsLoader),
|
|
123
|
-
])
|
|
445
|
+
]);
|
|
124
446
|
```
|
|
125
447
|
|
|
126
448
|
## Layout Loaders
|
|
@@ -186,37 +508,138 @@ function ProductPage() {
|
|
|
186
508
|
}
|
|
187
509
|
```
|
|
188
510
|
|
|
511
|
+
## Fetchable Loaders
|
|
512
|
+
|
|
513
|
+
By default, loaders only run during SSR and navigation. Pass `true` as the second
|
|
514
|
+
argument to `createLoader` to make a loader **fetchable** — callable from the client
|
|
515
|
+
via `useFetchLoader()` and `load()`:
|
|
516
|
+
|
|
517
|
+
```typescript
|
|
518
|
+
import { createLoader } from "@rangojs/router";
|
|
519
|
+
|
|
520
|
+
export const SearchLoader = createLoader(async (ctx) => {
|
|
521
|
+
"use server";
|
|
522
|
+
|
|
523
|
+
const query = ctx.params.query ?? "";
|
|
524
|
+
const results = await ctx.env.DB.prepare(
|
|
525
|
+
"SELECT * FROM products WHERE name LIKE ?",
|
|
526
|
+
)
|
|
527
|
+
.bind(`%${query}%`)
|
|
528
|
+
.all();
|
|
529
|
+
|
|
530
|
+
return { results: results.results ?? [] };
|
|
531
|
+
}, true); // true = fetchable
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
### Fetchable Loader with Middleware
|
|
535
|
+
|
|
536
|
+
Pass an options object instead of `true` to attach per-loader middleware.
|
|
537
|
+
This middleware runs only on `_rsc_loader` fetch requests (client-side
|
|
538
|
+
`load()` / `useFetchLoader()` calls), not during SSR `ctx.use()` execution:
|
|
539
|
+
|
|
540
|
+
```typescript
|
|
541
|
+
import { createLoader } from "@rangojs/router";
|
|
542
|
+
import { authMiddleware } from "../middleware/auth";
|
|
543
|
+
import { rateLimitMiddleware } from "../middleware/rate-limit";
|
|
544
|
+
|
|
545
|
+
export const ProtectedLoader = createLoader(
|
|
546
|
+
async (ctx) => {
|
|
547
|
+
"use server";
|
|
548
|
+
|
|
549
|
+
const user = ctx.get("user");
|
|
550
|
+
return { orders: await db.orders.list(user.id) };
|
|
551
|
+
},
|
|
552
|
+
{ middleware: [authMiddleware, rateLimitMiddleware] },
|
|
553
|
+
);
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
The middleware uses the same `MiddlewareFn` signature as route/app middleware,
|
|
557
|
+
so you can reuse existing middleware functions directly.
|
|
558
|
+
|
|
559
|
+
Fetchable loaders support both GET and POST (PUT, PATCH, DELETE) from the client.
|
|
560
|
+
The `load()` function auto-detects the body type:
|
|
561
|
+
|
|
562
|
+
- **JSON body** (`body: { ... }`) — sent as `application/json`, available as `ctx.body`
|
|
563
|
+
- **FormData body** (`body: formData`) — sent as `multipart/form-data`, available as `ctx.formData`
|
|
564
|
+
|
|
565
|
+
### Mutation Context
|
|
566
|
+
|
|
567
|
+
When a fetchable loader receives a POST/PUT/PATCH/DELETE request, the context
|
|
568
|
+
includes additional fields depending on the body type:
|
|
569
|
+
|
|
570
|
+
```typescript
|
|
571
|
+
export const MutationLoader = createLoader(async (ctx) => {
|
|
572
|
+
"use server";
|
|
573
|
+
|
|
574
|
+
// JSON body — available as ctx.body (parsed object)
|
|
575
|
+
const data = ctx.body as { name: string; email: string };
|
|
576
|
+
|
|
577
|
+
// FormData body — available as ctx.formData
|
|
578
|
+
const file = ctx.formData?.get("file") as File | null;
|
|
579
|
+
const name = ctx.formData?.get("name") as string | null;
|
|
580
|
+
|
|
581
|
+
// Route params are always available
|
|
582
|
+
const { slug } = ctx.params;
|
|
583
|
+
|
|
584
|
+
return { success: true };
|
|
585
|
+
}, true);
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
### File Upload Example
|
|
589
|
+
|
|
590
|
+
```typescript
|
|
591
|
+
// loaders/upload.ts
|
|
592
|
+
import { createLoader } from "@rangojs/router";
|
|
593
|
+
|
|
594
|
+
export const FileUploadLoader = createLoader(async (ctx) => {
|
|
595
|
+
"use server";
|
|
596
|
+
|
|
597
|
+
const file = ctx.formData?.get("file") as File | null;
|
|
598
|
+
if (file && file.size > 0) {
|
|
599
|
+
// Save to R2, D1, etc.
|
|
600
|
+
await ctx.env.BUCKET.put(file.name, file.stream());
|
|
601
|
+
return { uploaded: { name: file.name, size: file.size, type: file.type } };
|
|
602
|
+
}
|
|
603
|
+
return { uploaded: null };
|
|
604
|
+
}, true);
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
Client usage — see `/hooks useFetchLoader` for the full client-side pattern.
|
|
608
|
+
|
|
189
609
|
## Complete Example
|
|
190
610
|
|
|
191
611
|
```typescript
|
|
192
612
|
// loaders/shop.ts
|
|
193
613
|
import { createLoader } from "@rangojs/router";
|
|
194
614
|
|
|
195
|
-
export const ProductLoader = createLoader(
|
|
196
|
-
|
|
615
|
+
export const ProductLoader = createLoader(async (ctx) => {
|
|
616
|
+
"use server";
|
|
617
|
+
|
|
618
|
+
const product = await ctx.env.DB
|
|
197
619
|
.prepare("SELECT * FROM products WHERE slug = ?")
|
|
198
620
|
.bind(ctx.params.slug)
|
|
199
621
|
.first();
|
|
200
622
|
|
|
201
623
|
if (!product) {
|
|
202
|
-
|
|
624
|
+
notFound("Product not found");
|
|
203
625
|
}
|
|
204
626
|
|
|
205
627
|
return { product };
|
|
206
628
|
});
|
|
207
629
|
|
|
208
|
-
export const CartLoader = createLoader(
|
|
209
|
-
|
|
630
|
+
export const CartLoader = createLoader(async (ctx) => {
|
|
631
|
+
"use server";
|
|
632
|
+
|
|
633
|
+
const user = ctx.get("user");
|
|
210
634
|
if (!user) return { cart: null };
|
|
211
635
|
|
|
212
|
-
const cart = await ctx.env.
|
|
636
|
+
const cart = await ctx.env.KV.get(`cart:${user.id}`, "json");
|
|
213
637
|
return { cart };
|
|
214
638
|
});
|
|
215
639
|
|
|
216
|
-
// urls.tsx
|
|
640
|
+
// urls.tsx — register loaders in the DSL
|
|
217
641
|
export const urlpatterns = urls(({ path, layout, loader, loading, cache, revalidate }) => [
|
|
218
642
|
layout(<ShopLayout />, () => [
|
|
219
|
-
// Shared cart loader for all shop routes
|
|
220
643
|
loader(CartLoader, () => [
|
|
221
644
|
revalidate(({ actionId }) => actionId?.includes("Cart") ?? false),
|
|
222
645
|
]),
|
|
@@ -228,18 +651,22 @@ export const urlpatterns = urls(({ path, layout, loader, loading, cache, revalid
|
|
|
228
651
|
]),
|
|
229
652
|
]);
|
|
230
653
|
|
|
231
|
-
//
|
|
232
|
-
|
|
654
|
+
// components/ProductDetails.tsx — consume in client component
|
|
655
|
+
"use client";
|
|
656
|
+
import { useLoader } from "@rangojs/router/client";
|
|
233
657
|
import { ProductLoader, CartLoader } from "./loaders/shop";
|
|
234
658
|
|
|
235
|
-
|
|
236
|
-
const { product } =
|
|
237
|
-
const { cart } =
|
|
659
|
+
function ProductDetails() {
|
|
660
|
+
const { data: { product } } = useLoader(ProductLoader);
|
|
661
|
+
const { data: { cart } } = useLoader(CartLoader);
|
|
238
662
|
|
|
239
663
|
return (
|
|
240
664
|
<div>
|
|
241
665
|
<h1>{product.name}</h1>
|
|
242
|
-
<AddToCartButton
|
|
666
|
+
<AddToCartButton
|
|
667
|
+
productId={product.id}
|
|
668
|
+
inCart={cart?.items.includes(product.id)}
|
|
669
|
+
/>
|
|
243
670
|
</div>
|
|
244
671
|
);
|
|
245
672
|
}
|