@rangojs/router 0.0.0-experimental.2

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 (155) hide show
  1. package/CLAUDE.md +7 -0
  2. package/README.md +19 -0
  3. package/dist/vite/index.js +1298 -0
  4. package/package.json +140 -0
  5. package/skills/caching/SKILL.md +319 -0
  6. package/skills/document-cache/SKILL.md +152 -0
  7. package/skills/hooks/SKILL.md +359 -0
  8. package/skills/intercept/SKILL.md +292 -0
  9. package/skills/layout/SKILL.md +216 -0
  10. package/skills/loader/SKILL.md +365 -0
  11. package/skills/middleware/SKILL.md +442 -0
  12. package/skills/parallel/SKILL.md +255 -0
  13. package/skills/route/SKILL.md +141 -0
  14. package/skills/router-setup/SKILL.md +403 -0
  15. package/skills/theme/SKILL.md +54 -0
  16. package/skills/typesafety/SKILL.md +352 -0
  17. package/src/__mocks__/version.ts +6 -0
  18. package/src/__tests__/component-utils.test.ts +76 -0
  19. package/src/__tests__/route-definition.test.ts +63 -0
  20. package/src/__tests__/urls.test.tsx +436 -0
  21. package/src/browser/event-controller.ts +876 -0
  22. package/src/browser/index.ts +18 -0
  23. package/src/browser/link-interceptor.ts +121 -0
  24. package/src/browser/lru-cache.ts +69 -0
  25. package/src/browser/merge-segment-loaders.ts +126 -0
  26. package/src/browser/navigation-bridge.ts +893 -0
  27. package/src/browser/navigation-client.ts +162 -0
  28. package/src/browser/navigation-store.ts +823 -0
  29. package/src/browser/partial-update.ts +559 -0
  30. package/src/browser/react/Link.tsx +248 -0
  31. package/src/browser/react/NavigationProvider.tsx +275 -0
  32. package/src/browser/react/ScrollRestoration.tsx +94 -0
  33. package/src/browser/react/context.ts +53 -0
  34. package/src/browser/react/index.ts +52 -0
  35. package/src/browser/react/location-state-shared.ts +120 -0
  36. package/src/browser/react/location-state.ts +62 -0
  37. package/src/browser/react/use-action.ts +240 -0
  38. package/src/browser/react/use-client-cache.ts +56 -0
  39. package/src/browser/react/use-handle.ts +178 -0
  40. package/src/browser/react/use-href.tsx +208 -0
  41. package/src/browser/react/use-link-status.ts +134 -0
  42. package/src/browser/react/use-navigation.ts +150 -0
  43. package/src/browser/react/use-segments.ts +188 -0
  44. package/src/browser/request-controller.ts +164 -0
  45. package/src/browser/rsc-router.tsx +353 -0
  46. package/src/browser/scroll-restoration.ts +324 -0
  47. package/src/browser/server-action-bridge.ts +747 -0
  48. package/src/browser/shallow.ts +35 -0
  49. package/src/browser/types.ts +464 -0
  50. package/src/cache/__tests__/document-cache.test.ts +522 -0
  51. package/src/cache/__tests__/memory-segment-store.test.ts +487 -0
  52. package/src/cache/__tests__/memory-store.test.ts +484 -0
  53. package/src/cache/cache-scope.ts +565 -0
  54. package/src/cache/cf/__tests__/cf-cache-store.test.ts +428 -0
  55. package/src/cache/cf/cf-cache-store.ts +428 -0
  56. package/src/cache/cf/index.ts +19 -0
  57. package/src/cache/document-cache.ts +340 -0
  58. package/src/cache/index.ts +58 -0
  59. package/src/cache/memory-segment-store.ts +150 -0
  60. package/src/cache/memory-store.ts +253 -0
  61. package/src/cache/types.ts +387 -0
  62. package/src/client.rsc.tsx +88 -0
  63. package/src/client.tsx +621 -0
  64. package/src/component-utils.ts +76 -0
  65. package/src/components/DefaultDocument.tsx +23 -0
  66. package/src/default-error-boundary.tsx +88 -0
  67. package/src/deps/browser.ts +8 -0
  68. package/src/deps/html-stream-client.ts +2 -0
  69. package/src/deps/html-stream-server.ts +2 -0
  70. package/src/deps/rsc.ts +10 -0
  71. package/src/deps/ssr.ts +2 -0
  72. package/src/errors.ts +259 -0
  73. package/src/handle.ts +120 -0
  74. package/src/handles/MetaTags.tsx +193 -0
  75. package/src/handles/index.ts +6 -0
  76. package/src/handles/meta.ts +247 -0
  77. package/src/href-client.ts +128 -0
  78. package/src/href-context.ts +33 -0
  79. package/src/href.ts +177 -0
  80. package/src/index.rsc.ts +79 -0
  81. package/src/index.ts +87 -0
  82. package/src/loader.rsc.ts +204 -0
  83. package/src/loader.ts +47 -0
  84. package/src/network-error-thrower.tsx +21 -0
  85. package/src/outlet-context.ts +15 -0
  86. package/src/root-error-boundary.tsx +277 -0
  87. package/src/route-content-wrapper.tsx +198 -0
  88. package/src/route-definition.ts +1371 -0
  89. package/src/route-map-builder.ts +146 -0
  90. package/src/route-types.ts +198 -0
  91. package/src/route-utils.ts +89 -0
  92. package/src/router/__tests__/match-context.test.ts +104 -0
  93. package/src/router/__tests__/match-pipelines.test.ts +537 -0
  94. package/src/router/__tests__/match-result.test.ts +566 -0
  95. package/src/router/__tests__/on-error.test.ts +935 -0
  96. package/src/router/__tests__/pattern-matching.test.ts +577 -0
  97. package/src/router/error-handling.ts +287 -0
  98. package/src/router/handler-context.ts +158 -0
  99. package/src/router/loader-resolution.ts +326 -0
  100. package/src/router/manifest.ts +138 -0
  101. package/src/router/match-context.ts +264 -0
  102. package/src/router/match-middleware/background-revalidation.ts +236 -0
  103. package/src/router/match-middleware/cache-lookup.ts +261 -0
  104. package/src/router/match-middleware/cache-store.ts +266 -0
  105. package/src/router/match-middleware/index.ts +81 -0
  106. package/src/router/match-middleware/intercept-resolution.ts +268 -0
  107. package/src/router/match-middleware/segment-resolution.ts +174 -0
  108. package/src/router/match-pipelines.ts +214 -0
  109. package/src/router/match-result.ts +214 -0
  110. package/src/router/metrics.ts +62 -0
  111. package/src/router/middleware.test.ts +1355 -0
  112. package/src/router/middleware.ts +748 -0
  113. package/src/router/pattern-matching.ts +272 -0
  114. package/src/router/revalidation.ts +190 -0
  115. package/src/router/router-context.ts +299 -0
  116. package/src/router/types.ts +96 -0
  117. package/src/router.ts +3876 -0
  118. package/src/rsc/__tests__/helpers.test.ts +175 -0
  119. package/src/rsc/handler.ts +1060 -0
  120. package/src/rsc/helpers.ts +64 -0
  121. package/src/rsc/index.ts +56 -0
  122. package/src/rsc/nonce.ts +18 -0
  123. package/src/rsc/types.ts +237 -0
  124. package/src/segment-system.tsx +456 -0
  125. package/src/server/__tests__/request-context.test.ts +171 -0
  126. package/src/server/context.ts +417 -0
  127. package/src/server/handle-store.ts +230 -0
  128. package/src/server/loader-registry.ts +174 -0
  129. package/src/server/request-context.ts +554 -0
  130. package/src/server/root-layout.tsx +10 -0
  131. package/src/server/tsconfig.json +14 -0
  132. package/src/server.ts +146 -0
  133. package/src/ssr/__tests__/ssr-handler.test.tsx +188 -0
  134. package/src/ssr/index.tsx +234 -0
  135. package/src/theme/ThemeProvider.tsx +291 -0
  136. package/src/theme/ThemeScript.tsx +61 -0
  137. package/src/theme/__tests__/theme.test.ts +120 -0
  138. package/src/theme/constants.ts +55 -0
  139. package/src/theme/index.ts +58 -0
  140. package/src/theme/theme-context.ts +70 -0
  141. package/src/theme/theme-script.ts +152 -0
  142. package/src/theme/types.ts +182 -0
  143. package/src/theme/use-theme.ts +44 -0
  144. package/src/types.ts +1561 -0
  145. package/src/urls.ts +726 -0
  146. package/src/use-loader.tsx +346 -0
  147. package/src/vite/__tests__/expose-loader-id.test.ts +117 -0
  148. package/src/vite/expose-action-id.ts +344 -0
  149. package/src/vite/expose-handle-id.ts +209 -0
  150. package/src/vite/expose-loader-id.ts +357 -0
  151. package/src/vite/expose-location-state-id.ts +177 -0
  152. package/src/vite/index.ts +787 -0
  153. package/src/vite/package-resolution.ts +125 -0
  154. package/src/vite/version.d.ts +12 -0
  155. package/src/vite/virtual-entries.ts +109 -0
@@ -0,0 +1,216 @@
1
+ ---
2
+ name: layout
3
+ description: Define layout routes that wrap child routes in rsc-router
4
+ argument-hint: [layout-name]
5
+ ---
6
+
7
+ # Layout Routes
8
+
9
+ Layouts wrap child routes and stack vertically from parent to child.
10
+
11
+ ## Basic Layout
12
+
13
+ ```typescript
14
+ import { map } from "rsc-router/server";
15
+ import { Outlet } from "rsc-router";
16
+
17
+ // Layout component
18
+ function ShopLayout({ children }: { children: React.ReactNode }) {
19
+ return (
20
+ <div className="shop">
21
+ <ShopHeader />
22
+ <div className="shop-content">
23
+ {children}
24
+ </div>
25
+ <ShopFooter />
26
+ </div>
27
+ );
28
+ }
29
+
30
+ // Handler definition
31
+ export default map<typeof routes>(({ route, layout }) => [
32
+ layout(<ShopLayout />, () => [
33
+ route("index", ShopIndex),
34
+ route("products", ProductList),
35
+ route("cart", Cart),
36
+ ]),
37
+ ]);
38
+ ```
39
+
40
+ ## Nested Layouts
41
+
42
+ Layouts can be nested - they stack from parent to child:
43
+
44
+ ```typescript
45
+ export default map<typeof routes>(({ route, layout }) => [
46
+ // Root layout wraps everything
47
+ layout(<RootLayout />, () => [
48
+ route("index", HomePage),
49
+
50
+ // Shop layout nested inside root
51
+ layout(<ShopLayout />, () => [
52
+ route("shop.index", ShopIndex),
53
+ route("shop.products", ProductList),
54
+
55
+ // Account layout nested inside shop
56
+ layout(<AccountLayout />, () => [
57
+ route("shop.account.index", AccountIndex),
58
+ route("shop.account.orders", OrderList),
59
+ ]),
60
+ ]),
61
+
62
+ // Blog layout also inside root (sibling to shop)
63
+ layout(<BlogLayout />, () => [
64
+ route("blog.index", BlogIndex),
65
+ route("blog.post", BlogPost),
66
+ ]),
67
+ ]),
68
+ ]);
69
+ ```
70
+
71
+ Result for `/shop/account/orders`:
72
+ ```
73
+ <RootLayout>
74
+ <ShopLayout>
75
+ <AccountLayout>
76
+ <OrderList />
77
+ </AccountLayout>
78
+ </ShopLayout>
79
+ </RootLayout>
80
+ ```
81
+
82
+ ## Layout with Data Loading
83
+
84
+ Layouts can fetch data and use middleware:
85
+
86
+ ```typescript
87
+ export default map<typeof routes>(({ route, layout, loader, middleware }) => [
88
+ layout(
89
+ async (ctx) => {
90
+ const user = await ctx.use(UserLoader);
91
+ return (
92
+ <DashboardLayout user={user}>
93
+ <Outlet />
94
+ </DashboardLayout>
95
+ );
96
+ },
97
+ () => [
98
+ loader(UserLoader),
99
+ middleware(authMiddleware),
100
+
101
+ route("dashboard.index", DashboardIndex),
102
+ route("dashboard.settings", Settings),
103
+ ]
104
+ ),
105
+ ]);
106
+ ```
107
+
108
+ ## Orphan Layouts
109
+
110
+ A layout without children extends the parent layout:
111
+
112
+ ```typescript
113
+ layout(<RootLayout />, () => [
114
+ // This route uses RootLayout + ShopLayout
115
+ layout(<ShopLayout />),
116
+ route("shop.index", ShopIndex),
117
+
118
+ // This route only uses RootLayout
119
+ route("about", About),
120
+ ]);
121
+ ```
122
+
123
+ ## Layout with Parallel Slots
124
+
125
+ ```typescript
126
+ layout(
127
+ (ctx) => (
128
+ <DashboardLayout>
129
+ <ParallelOutlet name="@sidebar" />
130
+ <main>
131
+ <Outlet />
132
+ </main>
133
+ <ParallelOutlet name="@notifications" />
134
+ </DashboardLayout>
135
+ ),
136
+ () => [
137
+ parallel({
138
+ "@sidebar": () => <Sidebar />,
139
+ "@notifications": () => <NotificationPanel />,
140
+ }),
141
+ route("dashboard.index", DashboardIndex),
142
+ ]
143
+ )
144
+ ```
145
+
146
+ ## Layout with Handle Accumulation
147
+
148
+ ```typescript
149
+ import { createHandle } from "rsc-router";
150
+
151
+ export const Breadcrumbs = createHandle<{ label: string; href: string }>();
152
+
153
+ export default map<typeof routes>(({ route, layout }) => [
154
+ layout(
155
+ (ctx) => {
156
+ const push = ctx.use(Breadcrumbs);
157
+ push({ label: "Home", href: "/" });
158
+ return <RootLayout><Outlet /></RootLayout>;
159
+ },
160
+ () => [
161
+ layout(
162
+ (ctx) => {
163
+ const push = ctx.use(Breadcrumbs);
164
+ push({ label: "Shop", href: "/shop" });
165
+ return <ShopLayout><Outlet /></ShopLayout>;
166
+ },
167
+ () => [
168
+ route("shop.index", ShopIndex),
169
+ ]
170
+ ),
171
+ ]
172
+ ),
173
+ ]);
174
+
175
+ // In client component
176
+ function BreadcrumbNav() {
177
+ const crumbs = useHandle(Breadcrumbs);
178
+ // crumbs = [{ label: "Home", href: "/" }, { label: "Shop", href: "/shop" }]
179
+ }
180
+ ```
181
+
182
+ ## Layout Configuration Options
183
+
184
+ ```typescript
185
+ layout(<MyLayout />, () => [
186
+ loader(LayoutLoader), // Data for layout
187
+ middleware(authMiddleware), // Auth check before layout
188
+ loading(<LayoutSkeleton />), // Loading state
189
+ errorBoundary(<LayoutError />), // Error boundary
190
+ revalidate(shouldRevalidate), // Cache control
191
+
192
+ // Child routes
193
+ route("index", IndexHandler),
194
+ ])
195
+ ```
196
+
197
+ ## Layout Component Pattern
198
+
199
+ ```typescript
200
+ // layouts/ShopLayout.tsx
201
+ import { Outlet } from "rsc-router";
202
+
203
+ export function ShopLayout({ children }: { children?: React.ReactNode }) {
204
+ return (
205
+ <div className="shop-layout">
206
+ <ShopNav />
207
+ <main>
208
+ {children ?? <Outlet />}
209
+ </main>
210
+ <ShopFooter />
211
+ </div>
212
+ );
213
+ }
214
+ ```
215
+
216
+ Use `children` for direct children or `<Outlet />` for route content.
@@ -0,0 +1,365 @@
1
+ ---
2
+ name: loader
3
+ description: Define data loaders for fetching data in routes with createLoader
4
+ argument-hint: [loader-name]
5
+ ---
6
+
7
+ # Data Loaders
8
+
9
+ Loaders fetch data for routes and make it available to handlers and client components.
10
+
11
+ ## Creating a Loader
12
+
13
+ ```typescript
14
+ import { createLoader, notFound } from "rsc-router/server";
15
+
16
+ export const ProductLoader = createLoader(async (ctx) => {
17
+ const product = await db.products.findUnique({
18
+ where: { slug: ctx.params.slug },
19
+ });
20
+
21
+ if (!product) {
22
+ throw notFound("Product not found");
23
+ }
24
+
25
+ return product;
26
+ });
27
+ ```
28
+
29
+ ## Loader Context
30
+
31
+ The loader receives a context with:
32
+
33
+ ```typescript
34
+ export const MyLoader = createLoader(async (ctx) => {
35
+ ctx.params; // Route parameters { id: string, slug: string }
36
+ ctx.query; // Query string parameters
37
+ ctx.url; // Full URL object
38
+ ctx.pathname; // Current path
39
+ ctx.method; // HTTP method
40
+ ctx.request; // Raw Request object
41
+
42
+ // Variables from middleware
43
+ const user = ctx.get("user");
44
+
45
+ // Use other loaders
46
+ const related = await ctx.use(RelatedLoader);
47
+
48
+ return { user, related };
49
+ });
50
+ ```
51
+
52
+ ## Registering Loaders on Routes
53
+
54
+ ```typescript
55
+ import { map } from "rsc-router/server";
56
+ import { ProductLoader } from "../loaders/product";
57
+
58
+ export default map<typeof routes>(({ route, loader, loading }) => [
59
+ route("products.detail", async (ctx) => {
60
+ const product = await ctx.use(ProductLoader);
61
+ return <ProductPage product={product} />;
62
+ }, () => [
63
+ loader(ProductLoader),
64
+ loading(<ProductSkeleton />),
65
+ ]),
66
+ ]);
67
+ ```
68
+
69
+ ## Loader with Middleware
70
+
71
+ Add middleware specific to a loader:
72
+
73
+ ```typescript
74
+ export const UserProfileLoader = createLoader(
75
+ async (ctx) => {
76
+ const userId = ctx.get("validatedUserId");
77
+ return db.users.findUnique({ where: { id: userId } });
78
+ },
79
+ {
80
+ middleware: [
81
+ async (ctx, next) => {
82
+ const userId = ctx.params.id;
83
+
84
+ // Validate user ID
85
+ if (!isValidUUID(userId)) {
86
+ throw new Error("Invalid user ID");
87
+ }
88
+
89
+ ctx.set("validatedUserId", userId);
90
+ await next();
91
+ },
92
+ ],
93
+ }
94
+ );
95
+ ```
96
+
97
+ ## Using Loaders in Handlers
98
+
99
+ ```typescript
100
+ route("products.detail", async (ctx) => {
101
+ // Fetch loader data
102
+ const product = await ctx.use(ProductLoader);
103
+
104
+ // Multiple loaders
105
+ const [product, reviews, related] = await Promise.all([
106
+ ctx.use(ProductLoader),
107
+ ctx.use(ReviewsLoader),
108
+ ctx.use(RelatedProductsLoader),
109
+ ]);
110
+
111
+ return (
112
+ <ProductPage
113
+ product={product}
114
+ reviews={reviews}
115
+ related={related}
116
+ />
117
+ );
118
+ }, () => [
119
+ loader(ProductLoader),
120
+ loader(ReviewsLoader),
121
+ loader(RelatedProductsLoader),
122
+ ])
123
+ ```
124
+
125
+ ## Loader Revalidation
126
+
127
+ Control when loaders refetch:
128
+
129
+ ```typescript
130
+ route("products.detail", ProductHandler, () => [
131
+ loader(ProductLoader),
132
+
133
+ // Revalidate when params change
134
+ revalidate(({ currentParams, nextParams }) =>
135
+ currentParams.slug !== nextParams.slug
136
+ ),
137
+
138
+ // Revalidate on specific actions
139
+ revalidate(({ actionId }) =>
140
+ actionId?.includes("updateProduct") ?? false
141
+ ),
142
+ ])
143
+ ```
144
+
145
+ ### Soft vs Hard Revalidation
146
+
147
+ ```typescript
148
+ // Hard decision - stops evaluation
149
+ revalidate(({ currentParams, nextParams }) => {
150
+ return currentParams.id !== nextParams.id; // boolean
151
+ });
152
+
153
+ // Soft decision - continues to next revalidator
154
+ revalidate(({ actionId, defaultShouldRevalidate }) => {
155
+ if (actionId?.includes("cart")) {
156
+ return true; // Hard: must revalidate
157
+ }
158
+ // Soft: defer to next revalidator
159
+ return { defaultShouldRevalidate };
160
+ });
161
+ ```
162
+
163
+ ## Client-Side Loader Access
164
+
165
+ ### useLoader() - Strict access
166
+
167
+ ```tsx
168
+ "use client";
169
+ import { useLoader } from "rsc-router";
170
+ import { ProductLoader } from "../loaders/product";
171
+
172
+ function ProductPrice() {
173
+ // Data guaranteed (throws if not in context)
174
+ const { data } = useLoader(ProductLoader);
175
+ return <span>${data.price}</span>;
176
+ }
177
+ ```
178
+
179
+ ### useFetchLoader() - Flexible access
180
+
181
+ ```tsx
182
+ "use client";
183
+ import { useFetchLoader } from "rsc-router";
184
+ import { SearchLoader } from "../loaders/search";
185
+
186
+ function SearchResults() {
187
+ const { data, load, isLoading } = useFetchLoader(SearchLoader);
188
+
189
+ const handleSearch = async (query: string) => {
190
+ await load({ params: { query } });
191
+ };
192
+
193
+ return (
194
+ <div>
195
+ <input onChange={(e) => handleSearch(e.target.value)} />
196
+ {isLoading && <Spinner />}
197
+ {data?.results.map(r => <Result key={r.id} {...r} />)}
198
+ </div>
199
+ );
200
+ }
201
+ ```
202
+
203
+ ## Async/Streaming Loaders
204
+
205
+ Return promises for streaming:
206
+
207
+ ```typescript
208
+ export const RecommendationsLoader = createLoader(async (ctx) => {
209
+ return {
210
+ // Immediate data
211
+ product: await db.products.findUnique({ where: { id: ctx.params.id } }),
212
+
213
+ // Streams while other content renders
214
+ recommendations: db.recommendations.findAsync(ctx.params.id),
215
+ };
216
+ });
217
+ ```
218
+
219
+ ## Loader Caching
220
+
221
+ ```typescript
222
+ route("products.detail", ProductHandler, () => [
223
+ loader(ProductLoader, () => [
224
+ // Cache this loader's results
225
+ revalidate(({ currentParams, nextParams }) =>
226
+ currentParams.slug !== nextParams.slug
227
+ ),
228
+ ]),
229
+ ])
230
+
231
+ // Or use cache boundaries
232
+ cache({ ttl: 60, swr: 300 }, () => [
233
+ loader(ProductLoader),
234
+ route("products.detail", ProductHandler),
235
+ ])
236
+ ```
237
+
238
+ ## Loader Error Handling
239
+
240
+ ```typescript
241
+ export const ProductLoader = createLoader(async (ctx) => {
242
+ try {
243
+ const product = await db.products.findUnique({
244
+ where: { slug: ctx.params.slug },
245
+ });
246
+
247
+ if (!product) {
248
+ throw notFound("Product not found");
249
+ }
250
+
251
+ return product;
252
+ } catch (error) {
253
+ if (error instanceof DatabaseError) {
254
+ throw new Error("Failed to load product");
255
+ }
256
+ throw error;
257
+ }
258
+ });
259
+
260
+ // Handle in route
261
+ route("products.detail", ProductHandler, () => [
262
+ loader(ProductLoader),
263
+ errorBoundary(({ error, reset }) => (
264
+ <div>
265
+ <p>Error loading product: {error.message}</p>
266
+ <button onClick={reset}>Retry</button>
267
+ </div>
268
+ )),
269
+ ])
270
+ ```
271
+
272
+ ## Parallel Slot Loaders
273
+
274
+ ```typescript
275
+ parallel(
276
+ {
277
+ "@sidebar": async (ctx) => {
278
+ const categories = await ctx.use(CategoriesLoader);
279
+ return <CategorySidebar categories={categories} />;
280
+ },
281
+ },
282
+ () => [
283
+ loader(CategoriesLoader),
284
+ loading(<SidebarSkeleton />),
285
+ revalidate(({ actionId }) => actionId?.includes("category") ?? false),
286
+ ]
287
+ )
288
+ ```
289
+
290
+ ## Loader Composition
291
+
292
+ ```typescript
293
+ // Base loader
294
+ export const UserLoader = createLoader(async (ctx) => {
295
+ return db.users.findUnique({ where: { id: ctx.get("userId") } });
296
+ });
297
+
298
+ // Composed loader
299
+ export const UserWithOrdersLoader = createLoader(async (ctx) => {
300
+ const user = await ctx.use(UserLoader);
301
+ const orders = await db.orders.findMany({
302
+ where: { userId: user.id },
303
+ });
304
+
305
+ return { user, orders };
306
+ });
307
+ ```
308
+
309
+ ## Loader Type Safety
310
+
311
+ ```typescript
312
+ // Loader type is inferred
313
+ export const ProductLoader = createLoader(async (ctx) => {
314
+ return { id: "1", name: "Widget", price: 99 };
315
+ });
316
+
317
+ // In handler - type is { id: string; name: string; price: number }
318
+ const product = await ctx.use(ProductLoader);
319
+
320
+ // In client - same type
321
+ const { data } = useLoader(ProductLoader);
322
+ // data: { id: string; name: string; price: number }
323
+ ```
324
+
325
+ ## Common Patterns
326
+
327
+ ### List with pagination
328
+
329
+ ```typescript
330
+ export const ProductListLoader = createLoader(async (ctx) => {
331
+ const page = parseInt(ctx.query.page ?? "1");
332
+ const limit = 20;
333
+
334
+ const [products, total] = await Promise.all([
335
+ db.products.findMany({
336
+ skip: (page - 1) * limit,
337
+ take: limit,
338
+ }),
339
+ db.products.count(),
340
+ ]);
341
+
342
+ return {
343
+ products,
344
+ pagination: {
345
+ page,
346
+ totalPages: Math.ceil(total / limit),
347
+ total,
348
+ },
349
+ };
350
+ });
351
+ ```
352
+
353
+ ### Conditional loading
354
+
355
+ ```typescript
356
+ export const AdminDataLoader = createLoader(async (ctx) => {
357
+ const user = ctx.get("user");
358
+
359
+ if (!user || user.role !== "admin") {
360
+ return null;
361
+ }
362
+
363
+ return db.adminStats.get();
364
+ });
365
+ ```