@funstack/router 0.0.5 → 0.0.7-alpha.0
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/dist/bin/skill-installer.d.mts +1 -0
- package/dist/bin/skill-installer.mjs +13 -0
- package/dist/bin/skill-installer.mjs.map +1 -0
- package/dist/docs/ApiComponentsPage.tsx +85 -0
- package/dist/docs/ApiHooksPage.tsx +323 -0
- package/dist/docs/ApiTypesPage.tsx +310 -0
- package/dist/docs/ApiUtilitiesPage.tsx +298 -0
- package/dist/docs/GettingStartedPage.tsx +186 -0
- package/dist/docs/LearnNavigationApiPage.tsx +255 -0
- package/dist/docs/LearnNestedRoutesPage.tsx +601 -0
- package/dist/docs/LearnRscPage.tsx +293 -0
- package/dist/docs/LearnSsrPage.tsx +180 -0
- package/dist/docs/LearnTransitionsPage.tsx +146 -0
- package/dist/docs/LearnTypeSafetyPage.tsx +522 -0
- package/dist/docs/index.md +21 -0
- package/dist/index.d.mts +2 -8
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +8 -27
- package/dist/index.mjs.map +1 -1
- package/dist/{route-Bc8BUlhv.d.mts → route-ClVnhrQD.d.mts} +1 -1
- package/dist/{route-Bc8BUlhv.d.mts.map → route-ClVnhrQD.d.mts.map} +1 -1
- package/dist/server.d.mts +1 -1
- package/package.json +11 -2
- package/skills/funstack-router-knowledge/SKILL.md +21 -0
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
import { CodeBlock } from "../components/CodeBlock.js";
|
|
2
|
+
|
|
3
|
+
export function LearnTypeSafetyPage() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="learn-content">
|
|
6
|
+
<h2>Type Safety</h2>
|
|
7
|
+
|
|
8
|
+
<p className="page-intro">
|
|
9
|
+
FUNSTACK Router provides first-class TypeScript support, allowing you to
|
|
10
|
+
access route params, navigation state, and loader data with full type
|
|
11
|
+
safety. This guide covers two approaches: receiving typed data through
|
|
12
|
+
component props (recommended) and accessing it through hooks.
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
<section>
|
|
16
|
+
<h3>Why Type Safety Matters</h3>
|
|
17
|
+
<p>
|
|
18
|
+
Routing is one of the most common sources of runtime errors in web
|
|
19
|
+
applications. Typos in parameter names, incorrect assumptions about
|
|
20
|
+
data shapes, or forgetting to handle navigation state can lead to
|
|
21
|
+
subtle bugs that are hard to track down.
|
|
22
|
+
</p>
|
|
23
|
+
<p>
|
|
24
|
+
With FUNSTACK Router's type-safe approach, the TypeScript compiler
|
|
25
|
+
catches these errors at build time. You get autocomplete for parameter
|
|
26
|
+
names, type checking for loader data, and confidence that your route
|
|
27
|
+
components receive exactly the data they expect.
|
|
28
|
+
</p>
|
|
29
|
+
<p>There are two ways to access typed route data:</p>
|
|
30
|
+
<ul>
|
|
31
|
+
<li>
|
|
32
|
+
<strong>Props (Recommended)</strong> — Route components
|
|
33
|
+
receive typed data directly as props. This is the simplest and most
|
|
34
|
+
type-safe approach.
|
|
35
|
+
</li>
|
|
36
|
+
<li>
|
|
37
|
+
<strong>Hooks</strong> — Use hooks like{" "}
|
|
38
|
+
<code>useRouteParams</code> and <code>useRouteData</code> to access
|
|
39
|
+
data anywhere in the component tree. This requires routes to have an{" "}
|
|
40
|
+
<code>id</code> property.
|
|
41
|
+
</li>
|
|
42
|
+
</ul>
|
|
43
|
+
</section>
|
|
44
|
+
|
|
45
|
+
<section>
|
|
46
|
+
<h3>Approach 1: Route Component Props (Recommended)</h3>
|
|
47
|
+
|
|
48
|
+
<h4>Accessing Typed Params via Props</h4>
|
|
49
|
+
<p>
|
|
50
|
+
When you define a route with URL parameters, FUNSTACK Router
|
|
51
|
+
automatically infers the parameter types from the path pattern. Your
|
|
52
|
+
component receives these params as a typed <code>params</code> prop.
|
|
53
|
+
</p>
|
|
54
|
+
<CodeBlock language="tsx">{`import { route } from "@funstack/router";
|
|
55
|
+
|
|
56
|
+
// Route definition with :userId parameter
|
|
57
|
+
const userRoute = route({
|
|
58
|
+
path: "/users/:userId",
|
|
59
|
+
component: UserPage,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Component receives typed params automatically
|
|
63
|
+
function UserPage({ params }: { params: { userId: string } }) {
|
|
64
|
+
return <h1>User: {params.userId}</h1>;
|
|
65
|
+
}`}</CodeBlock>
|
|
66
|
+
<p>
|
|
67
|
+
For explicit type annotations, use the{" "}
|
|
68
|
+
<code>RouteComponentProps</code> type helper with your params type:
|
|
69
|
+
</p>
|
|
70
|
+
<CodeBlock language="tsx">{`import { route, RouteComponentProps } from "@funstack/router";
|
|
71
|
+
|
|
72
|
+
// Define component with explicit props type
|
|
73
|
+
function UserPage({ params }: RouteComponentProps<{ userId: string }>) {
|
|
74
|
+
// params.userId is typed as string
|
|
75
|
+
// params.nonExistent would be a TypeScript error
|
|
76
|
+
return <h1>User: {params.userId}</h1>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Route definition - TypeScript validates the component props match the path
|
|
80
|
+
const userRoute = route({
|
|
81
|
+
path: "/users/:userId",
|
|
82
|
+
component: UserPage,
|
|
83
|
+
});`}</CodeBlock>
|
|
84
|
+
<p>
|
|
85
|
+
The <code>route()</code> function validates that your component's
|
|
86
|
+
props match the path pattern. If you annotate <code>params</code> with{" "}
|
|
87
|
+
<code>{`{ userId: string }`}</code> but the path is{" "}
|
|
88
|
+
<code>/users/:id</code>, TypeScript will report an error.
|
|
89
|
+
</p>
|
|
90
|
+
|
|
91
|
+
<h4>Routes with Loaders</h4>
|
|
92
|
+
<p>
|
|
93
|
+
When your route has a loader function, the component receives the
|
|
94
|
+
loader's return value as a <code>data</code> prop. The data can be
|
|
95
|
+
wrapped in a Promise, in which case you unwrap it using React's{" "}
|
|
96
|
+
<code>use()</code> hook. Use <code>RouteComponentPropsWithData</code>{" "}
|
|
97
|
+
for routes with loaders.
|
|
98
|
+
</p>
|
|
99
|
+
<CodeBlock language="tsx">{`import { use, Suspense } from "react";
|
|
100
|
+
import { route, RouteComponentPropsWithData } from "@funstack/router";
|
|
101
|
+
|
|
102
|
+
interface User {
|
|
103
|
+
id: string;
|
|
104
|
+
name: string;
|
|
105
|
+
email: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Props type: RouteComponentPropsWithData<Params, Data, State?>
|
|
109
|
+
type UserPageProps = RouteComponentPropsWithData<
|
|
110
|
+
{ userId: string },
|
|
111
|
+
Promise<User>
|
|
112
|
+
>;
|
|
113
|
+
|
|
114
|
+
// Inner component that uses the data
|
|
115
|
+
function UserPageContent({ params, data }: UserPageProps) {
|
|
116
|
+
const user = use(data); // Unwrap the Promise
|
|
117
|
+
return (
|
|
118
|
+
<div>
|
|
119
|
+
<h1>{user.name}</h1>
|
|
120
|
+
<p>Email: {user.email}</p>
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Outer component wraps with Suspense
|
|
126
|
+
function UserPage(props: UserPageProps) {
|
|
127
|
+
return (
|
|
128
|
+
<Suspense fallback={<div>Loading user...</div>}>
|
|
129
|
+
<UserPageContent {...props} />
|
|
130
|
+
</Suspense>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Route definition
|
|
135
|
+
const userRoute = route({
|
|
136
|
+
path: "/users/:userId",
|
|
137
|
+
component: UserPage,
|
|
138
|
+
loader: async ({ params }): Promise<User> => {
|
|
139
|
+
const response = await fetch(\`/api/users/\${params.userId}\`);
|
|
140
|
+
return response.json();
|
|
141
|
+
},
|
|
142
|
+
});`}</CodeBlock>
|
|
143
|
+
<p>
|
|
144
|
+
The <code>data</code> prop is typed as{" "}
|
|
145
|
+
<code>Promise<User></code> based on the loader's return type.
|
|
146
|
+
TypeScript ensures you handle the data shape correctly.
|
|
147
|
+
</p>
|
|
148
|
+
|
|
149
|
+
<h4>Routes with Navigation State</h4>
|
|
150
|
+
<p>
|
|
151
|
+
Navigation state lets you store data in a navigation entry that
|
|
152
|
+
doesn't appear in the URL. Navigation state data is persisted across
|
|
153
|
+
page reloads and history traversals (meaning it is available after
|
|
154
|
+
user goes to another page and then uses the back button to returns to
|
|
155
|
+
the current page). Use the <code>routeState</code> helper to define
|
|
156
|
+
typed state for your routes.
|
|
157
|
+
</p>
|
|
158
|
+
<CodeBlock language="tsx">{`import { route, routeState, RouteComponentProps } from "@funstack/router";
|
|
159
|
+
|
|
160
|
+
// Define the state shape
|
|
161
|
+
interface ProductListState {
|
|
162
|
+
page: number;
|
|
163
|
+
sortBy: "name" | "price" | "date";
|
|
164
|
+
filters: string[];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Props type: RouteComponentProps<Params, State>
|
|
168
|
+
type ProductListProps = RouteComponentProps<
|
|
169
|
+
Record<string, never>, // No params for this route
|
|
170
|
+
ProductListState
|
|
171
|
+
>;
|
|
172
|
+
|
|
173
|
+
function ProductListPage({
|
|
174
|
+
state,
|
|
175
|
+
setState,
|
|
176
|
+
setStateSync,
|
|
177
|
+
resetState,
|
|
178
|
+
}: ProductListProps) {
|
|
179
|
+
// state is typed as ProductListState | undefined
|
|
180
|
+
const page = state?.page ?? 1;
|
|
181
|
+
const sortBy = state?.sortBy ?? "name";
|
|
182
|
+
|
|
183
|
+
const handlePageChange = (newPage: number) => {
|
|
184
|
+
// setState performs a navigation with the new state
|
|
185
|
+
setState({ ...state, page: newPage });
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const handleSortChange = (newSort: "name" | "price" | "date") => {
|
|
189
|
+
// setStateSync updates state synchronously (replaces current entry)
|
|
190
|
+
setStateSync({ ...state, sortBy: newSort });
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const handleReset = () => {
|
|
194
|
+
// resetState clears the navigation state
|
|
195
|
+
resetState();
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
<div>
|
|
200
|
+
<button onClick={() => handlePageChange(page + 1)}>
|
|
201
|
+
Next Page
|
|
202
|
+
</button>
|
|
203
|
+
<button onClick={() => handleSortChange("price")}>
|
|
204
|
+
Sort by Price
|
|
205
|
+
</button>
|
|
206
|
+
<button onClick={handleReset}>Reset Filters</button>
|
|
207
|
+
</div>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Use routeState to create a typed route
|
|
212
|
+
const productListRoute = routeState<ProductListState>()(
|
|
213
|
+
route({
|
|
214
|
+
path: "/products",
|
|
215
|
+
component: ProductListPage,
|
|
216
|
+
})
|
|
217
|
+
);`}</CodeBlock>
|
|
218
|
+
<p>
|
|
219
|
+
The <code>routeState</code> helper adds four props to your component:
|
|
220
|
+
</p>
|
|
221
|
+
<ul>
|
|
222
|
+
<li>
|
|
223
|
+
<code>state</code> — The current navigation state (or{" "}
|
|
224
|
+
<code>undefined</code> if not set)
|
|
225
|
+
</li>
|
|
226
|
+
<li>
|
|
227
|
+
<code>setState</code> — Navigate to the same URL with new
|
|
228
|
+
state (creates a new history entry)
|
|
229
|
+
</li>
|
|
230
|
+
<li>
|
|
231
|
+
<code>setStateSync</code> — Update state synchronously without
|
|
232
|
+
creating a new history entry
|
|
233
|
+
</li>
|
|
234
|
+
<li>
|
|
235
|
+
<code>resetState</code> — Clear the navigation state
|
|
236
|
+
</li>
|
|
237
|
+
</ul>
|
|
238
|
+
|
|
239
|
+
<h4>Combining Loader and State</h4>
|
|
240
|
+
<p>
|
|
241
|
+
You can use both loaders and navigation state together. The{" "}
|
|
242
|
+
<code>routeState</code> helper works with routes that have loaders.
|
|
243
|
+
</p>
|
|
244
|
+
<CodeBlock language="tsx">{`import { use, Suspense } from "react";
|
|
245
|
+
import { route, routeState, RouteComponentPropsWithData } from "@funstack/router";
|
|
246
|
+
|
|
247
|
+
interface ProductListState {
|
|
248
|
+
sortBy: "name" | "price";
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
interface Product {
|
|
252
|
+
id: string;
|
|
253
|
+
name: string;
|
|
254
|
+
price: number;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Props type: RouteComponentPropsWithData<Params, Data, State>
|
|
258
|
+
type Props = RouteComponentPropsWithData<
|
|
259
|
+
Record<string, never>,
|
|
260
|
+
Promise<Product[]>,
|
|
261
|
+
ProductListState
|
|
262
|
+
>;
|
|
263
|
+
|
|
264
|
+
function ProductListContent({ data, state, setStateSync }: Props) {
|
|
265
|
+
const products = use(data);
|
|
266
|
+
const sortBy = state?.sortBy ?? "name";
|
|
267
|
+
|
|
268
|
+
const sorted = [...products].sort((a, b) =>
|
|
269
|
+
sortBy === "name"
|
|
270
|
+
? a.name.localeCompare(b.name)
|
|
271
|
+
: a.price - b.price
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
return (
|
|
275
|
+
<div>
|
|
276
|
+
<select
|
|
277
|
+
value={sortBy}
|
|
278
|
+
onChange={(e) =>
|
|
279
|
+
setStateSync({ sortBy: e.target.value as "name" | "price" })
|
|
280
|
+
}
|
|
281
|
+
>
|
|
282
|
+
<option value="name">Sort by Name</option>
|
|
283
|
+
<option value="price">Sort by Price</option>
|
|
284
|
+
</select>
|
|
285
|
+
<ul>
|
|
286
|
+
{sorted.map((product) => (
|
|
287
|
+
<li key={product.id}>
|
|
288
|
+
{product.name} - \${product.price}
|
|
289
|
+
</li>
|
|
290
|
+
))}
|
|
291
|
+
</ul>
|
|
292
|
+
</div>
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function ProductListPage(props: Props) {
|
|
297
|
+
return (
|
|
298
|
+
<Suspense fallback={<div>Loading products...</div>}>
|
|
299
|
+
<ProductListContent {...props} />
|
|
300
|
+
</Suspense>
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Route definition with both loader and state
|
|
305
|
+
const productListRoute = routeState<ProductListState>()(
|
|
306
|
+
route({
|
|
307
|
+
path: "/products",
|
|
308
|
+
component: ProductListPage,
|
|
309
|
+
loader: async (): Promise<Product[]> => {
|
|
310
|
+
const response = await fetch("/api/products");
|
|
311
|
+
return response.json();
|
|
312
|
+
},
|
|
313
|
+
})
|
|
314
|
+
);`}</CodeBlock>
|
|
315
|
+
</section>
|
|
316
|
+
|
|
317
|
+
<section>
|
|
318
|
+
<h3>Approach 2: Hooks</h3>
|
|
319
|
+
|
|
320
|
+
<h4>When to Use Hooks</h4>
|
|
321
|
+
<p>
|
|
322
|
+
While props are the recommended approach for most cases, hooks are
|
|
323
|
+
useful when:
|
|
324
|
+
</p>
|
|
325
|
+
<ul>
|
|
326
|
+
<li>
|
|
327
|
+
<strong>Avoiding prop drilling</strong> — Deeply nested
|
|
328
|
+
components need route data without passing props through every level
|
|
329
|
+
</li>
|
|
330
|
+
<li>
|
|
331
|
+
<strong>Accessing parent route data</strong> — Child routes
|
|
332
|
+
need to read data loaded by ancestor routes
|
|
333
|
+
</li>
|
|
334
|
+
<li>
|
|
335
|
+
<strong>Using React Server Components</strong> — Route
|
|
336
|
+
components cannot receive props directly
|
|
337
|
+
</li>
|
|
338
|
+
</ul>
|
|
339
|
+
<p>
|
|
340
|
+
<strong>Important:</strong> To use hooks with full type safety, routes
|
|
341
|
+
must have an <code>id</code> property.
|
|
342
|
+
</p>
|
|
343
|
+
|
|
344
|
+
<h4>Setting Up Routes with IDs</h4>
|
|
345
|
+
<p>
|
|
346
|
+
Add an <code>id</code> property to routes you want to access via
|
|
347
|
+
hooks. The ID can be any string, but using a descriptive name helps
|
|
348
|
+
with debugging.
|
|
349
|
+
</p>
|
|
350
|
+
<CodeBlock language="tsx">{`import { route } from "@funstack/router";
|
|
351
|
+
|
|
352
|
+
const userRoute = route({
|
|
353
|
+
id: "user",
|
|
354
|
+
path: "/users/:userId",
|
|
355
|
+
component: UserLayout,
|
|
356
|
+
loader: async ({ params }) => {
|
|
357
|
+
const response = await fetch(\`/api/users/\${params.userId}\`);
|
|
358
|
+
return response.json();
|
|
359
|
+
},
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
const userPostsRoute = route({
|
|
363
|
+
id: "userPosts",
|
|
364
|
+
path: "/posts",
|
|
365
|
+
component: UserPostsPage,
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// Use these routes in your route tree
|
|
369
|
+
const routes = [
|
|
370
|
+
route({
|
|
371
|
+
path: "/",
|
|
372
|
+
component: Layout,
|
|
373
|
+
children: [
|
|
374
|
+
{
|
|
375
|
+
...userRoute,
|
|
376
|
+
children: [userPostsRoute],
|
|
377
|
+
},
|
|
378
|
+
],
|
|
379
|
+
}),
|
|
380
|
+
];`}</CodeBlock>
|
|
381
|
+
|
|
382
|
+
<h4>useRouteParams</h4>
|
|
383
|
+
<p>
|
|
384
|
+
The <code>useRouteParams</code> hook returns typed params for a
|
|
385
|
+
specific route. It works with the current route or any ancestor route.
|
|
386
|
+
</p>
|
|
387
|
+
<CodeBlock language="tsx">{`import { useRouteParams } from "@funstack/router";
|
|
388
|
+
|
|
389
|
+
// In a deeply nested component
|
|
390
|
+
function UserAvatar() {
|
|
391
|
+
// Pass the route definition to get typed params
|
|
392
|
+
const params = useRouteParams(userRoute);
|
|
393
|
+
// params.userId is typed as string
|
|
394
|
+
|
|
395
|
+
return <img src={\`/avatars/\${params.userId}.png\`} alt="User avatar" />;
|
|
396
|
+
}`}</CodeBlock>
|
|
397
|
+
|
|
398
|
+
<h4>useRouteState</h4>
|
|
399
|
+
<p>
|
|
400
|
+
The <code>useRouteState</code> hook returns the typed navigation state
|
|
401
|
+
for a route. Returns <code>undefined</code> when no state is set.
|
|
402
|
+
</p>
|
|
403
|
+
<CodeBlock language="tsx">{`import { useRouteState } from "@funstack/router";
|
|
404
|
+
|
|
405
|
+
function FilterIndicator() {
|
|
406
|
+
// Get typed state from the product list route
|
|
407
|
+
const state = useRouteState(productListRoute);
|
|
408
|
+
// state is typed as ProductListState | undefined
|
|
409
|
+
|
|
410
|
+
if (!state?.filters?.length) {
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return (
|
|
415
|
+
<div className="filter-badge">
|
|
416
|
+
{state.filters.length} filters active
|
|
417
|
+
</div>
|
|
418
|
+
);
|
|
419
|
+
}`}</CodeBlock>
|
|
420
|
+
|
|
421
|
+
<h4>useRouteData</h4>
|
|
422
|
+
<p>
|
|
423
|
+
The <code>useRouteData</code> hook returns the typed loader data for a
|
|
424
|
+
route. This is particularly useful for accessing parent route data
|
|
425
|
+
from child routes.
|
|
426
|
+
</p>
|
|
427
|
+
<CodeBlock language="tsx">{`import { use } from "react";
|
|
428
|
+
import { useRouteData } from "@funstack/router";
|
|
429
|
+
|
|
430
|
+
// Child route component accessing parent's data
|
|
431
|
+
function UserPostsPage() {
|
|
432
|
+
// Access the parent route's loaded user data
|
|
433
|
+
const userData = useRouteData(userRoute);
|
|
434
|
+
const user = use(userData);
|
|
435
|
+
|
|
436
|
+
return (
|
|
437
|
+
<div>
|
|
438
|
+
<h2>Posts by {user.name}</h2>
|
|
439
|
+
{/* Render posts... */}
|
|
440
|
+
</div>
|
|
441
|
+
);
|
|
442
|
+
}`}</CodeBlock>
|
|
443
|
+
<p>
|
|
444
|
+
This pattern is especially powerful in nested routes where child
|
|
445
|
+
components need access to data loaded by parent routes without prop
|
|
446
|
+
drilling.
|
|
447
|
+
</p>
|
|
448
|
+
</section>
|
|
449
|
+
|
|
450
|
+
<section>
|
|
451
|
+
<h3>Route Definition Best Practices</h3>
|
|
452
|
+
<p>
|
|
453
|
+
To maximize developer experience and maintainability while also
|
|
454
|
+
ensuring type safety, follow the below best practices when defining
|
|
455
|
+
your routes:
|
|
456
|
+
</p>
|
|
457
|
+
<CodeBlock language="tsx">{`interface User {
|
|
458
|
+
name: string;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Define params and data
|
|
462
|
+
type Params = { userId: string };
|
|
463
|
+
type Data = Promise<User>;
|
|
464
|
+
|
|
465
|
+
// Use RouteComponentProps (or RouteComponentPropsWithData) to type your route component
|
|
466
|
+
type UserPageProps = RouteComponentPropsWithData<Params, Data>;
|
|
467
|
+
|
|
468
|
+
// Define the route
|
|
469
|
+
const userRoute = route({
|
|
470
|
+
path: "/users/:userId",
|
|
471
|
+
loader: async ({ params }): Data => {
|
|
472
|
+
const response = await fetch(\`/api/users/\${params.userId}\`);
|
|
473
|
+
return response.json();
|
|
474
|
+
},
|
|
475
|
+
component: UserPage,
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
// Now use it in your component
|
|
479
|
+
function UserPage({ params, data }: UserPageProps) {
|
|
480
|
+
const user = use(data);
|
|
481
|
+
return <h1>User: {user.name} (ID: {params.userId})</h1>;
|
|
482
|
+
}`}</CodeBlock>
|
|
483
|
+
<p>Key techniques demonstrated here include:</p>
|
|
484
|
+
<ul>
|
|
485
|
+
<li>
|
|
486
|
+
Defining explicit <code>Params</code> and <code>Data</code> types
|
|
487
|
+
— requires minimal type checking effort while improving
|
|
488
|
+
clarity
|
|
489
|
+
</li>
|
|
490
|
+
<li>
|
|
491
|
+
Using <code>RouteComponentPropsWithData</code> to define component
|
|
492
|
+
props
|
|
493
|
+
</li>
|
|
494
|
+
</ul>
|
|
495
|
+
<p>
|
|
496
|
+
TypeScript will validate that the route definition and component props
|
|
497
|
+
remain in sync as you make changes over time.
|
|
498
|
+
</p>
|
|
499
|
+
</section>
|
|
500
|
+
|
|
501
|
+
<section>
|
|
502
|
+
<h3>Key Takeaways</h3>
|
|
503
|
+
<ul>
|
|
504
|
+
<li>
|
|
505
|
+
Use <code>RouteComponentProps<Params, State></code> or{" "}
|
|
506
|
+
<code>RouteComponentPropsWithData<Params, Data, State></code>{" "}
|
|
507
|
+
for constructing route component prop types
|
|
508
|
+
</li>
|
|
509
|
+
<li>
|
|
510
|
+
The <code>routeState</code> helper adds typed route state management
|
|
511
|
+
to any route
|
|
512
|
+
</li>
|
|
513
|
+
<li>
|
|
514
|
+
Hooks require routes to have an <code>id</code> property for type
|
|
515
|
+
safety
|
|
516
|
+
</li>
|
|
517
|
+
<li>Use hooks to avoid prop drilling or access parent route data</li>
|
|
518
|
+
</ul>
|
|
519
|
+
</section>
|
|
520
|
+
</div>
|
|
521
|
+
);
|
|
522
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# FUNSTACK Router Documentation
|
|
2
|
+
|
|
3
|
+
## Available Documentation
|
|
4
|
+
|
|
5
|
+
- [Getting Started](./GettingStartedPage.tsx)
|
|
6
|
+
|
|
7
|
+
### API Reference
|
|
8
|
+
|
|
9
|
+
- [Components](./ApiComponentsPage.tsx) - Core components for building routing in your React application.
|
|
10
|
+
- [Hooks](./ApiHooksPage.tsx) - React hooks for accessing router state and navigation.
|
|
11
|
+
- [Types](./ApiTypesPage.tsx) - TypeScript types and interfaces exported by the router.
|
|
12
|
+
- [Utilities](./ApiUtilitiesPage.tsx) - Helper functions for defining routes and managing state.
|
|
13
|
+
|
|
14
|
+
### Learn
|
|
15
|
+
|
|
16
|
+
- [Navigation API](./LearnNavigationApiPage.tsx) - FUNSTACK Router is built on the Navigation API , a modern browser API that provides a unified way to handle navigation. This guide explains the key differences from the older History API and the benefits this brings to your application.
|
|
17
|
+
- [Nested Routes](./LearnNestedRoutesPage.tsx) - Nested routes let you build complex page layouts where parts of the UI persist across navigation while other parts change. Think of a dashboard with a sidebar that stays in place while the main content area updates—that's nested routing in action.
|
|
18
|
+
- [React Server Components](./LearnRscPage.tsx) - FUNSTACK Router is designed to work with React Server Components (RSC). The package provides a dedicated server entry point so that route definitions can live in server modules, keeping client bundle sizes small.
|
|
19
|
+
- [Server-Side Rendering](./LearnSsrPage.tsx) - FUNSTACK Router supports server-side rendering with a two-stage model. During SSR, pathless (layout) routes without loaders render to produce an app shell, while path-based routes and loaders activate only after client hydration.
|
|
20
|
+
- [Controlling Transitions](./LearnTransitionsPage.tsx) - FUNSTACK Router wraps every navigation in React's startTransition, which means the old UI may stay visible while the new route loads. This page explains how this works and how to control it.
|
|
21
|
+
- [Type Safety](./LearnTypeSafetyPage.tsx) - FUNSTACK Router provides first-class TypeScript support, allowing you to access route params, navigation state, and loader data with full type safety. This guide covers two approaches: receiving typed data through component props (recommended) and accessing it through hooks.
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as LoaderArgs, c as RouteComponentProps, d as RouteDefinition, f as TypefulOpaqueRouteDefinition, i as ExtractRouteState, l as RouteComponentPropsOf, m as routeState, n as ExtractRouteId, o as OpaqueRouteDefinition, p as route, r as ExtractRouteParams, s as PathParams, t as ExtractRouteData, u as RouteComponentPropsWithData } from "./route-
|
|
1
|
+
import { a as LoaderArgs, c as RouteComponentProps, d as RouteDefinition, f as TypefulOpaqueRouteDefinition, i as ExtractRouteState, l as RouteComponentPropsOf, m as routeState, n as ExtractRouteId, o as OpaqueRouteDefinition, p as route, r as ExtractRouteParams, s as PathParams, t as ExtractRouteData, u as RouteComponentPropsWithData } from "./route-ClVnhrQD.mjs";
|
|
2
2
|
import { ComponentType, ReactNode } from "react";
|
|
3
3
|
|
|
4
4
|
//#region src/types.d.ts
|
|
@@ -136,12 +136,6 @@ declare function useNavigate(): (to: string, options?: NavigateOptions) => void;
|
|
|
136
136
|
*/
|
|
137
137
|
declare function useLocation(): Location;
|
|
138
138
|
//#endregion
|
|
139
|
-
//#region src/hooks/useLocationSSR.d.ts
|
|
140
|
-
/**
|
|
141
|
-
* Returns the current location object, or `null` when the URL is not available (e.g. during SSR).
|
|
142
|
-
*/
|
|
143
|
-
declare function useLocationSSR(): Location | null;
|
|
144
|
-
//#endregion
|
|
145
139
|
//#region src/hooks/useSearchParams.d.ts
|
|
146
140
|
type SetSearchParams = (params: URLSearchParams | Record<string, string> | ((prev: URLSearchParams) => URLSearchParams | Record<string, string>)) => void;
|
|
147
141
|
/**
|
|
@@ -278,5 +272,5 @@ type LocationEntry = {
|
|
|
278
272
|
info: unknown;
|
|
279
273
|
};
|
|
280
274
|
//#endregion
|
|
281
|
-
export { type ExtractRouteData, type ExtractRouteId, type ExtractRouteParams, type ExtractRouteState, type FallbackMode, type LoaderArgs, type Location, type LocationEntry, type MatchedRoute, type MatchedRouteWithData, type NavigateOptions, type OnNavigateCallback, type OnNavigateInfo, type OpaqueRouteDefinition, Outlet, type PathParams, type RouteComponentProps, type RouteComponentPropsOf, type RouteComponentPropsWithData, type RouteDefinition, Router, type RouterProps, type TypefulOpaqueRouteDefinition, type UseBlockerOptions, route, routeState, useBlocker, useIsPending, useLocation,
|
|
275
|
+
export { type ExtractRouteData, type ExtractRouteId, type ExtractRouteParams, type ExtractRouteState, type FallbackMode, type LoaderArgs, type Location, type LocationEntry, type MatchedRoute, type MatchedRouteWithData, type NavigateOptions, type OnNavigateCallback, type OnNavigateInfo, type OpaqueRouteDefinition, Outlet, type PathParams, type RouteComponentProps, type RouteComponentPropsOf, type RouteComponentPropsWithData, type RouteDefinition, Router, type RouterProps, type TypefulOpaqueRouteDefinition, type UseBlockerOptions, route, routeState, useBlocker, useIsPending, useLocation, useNavigate, useRouteData, useRouteParams, useRouteState, useSearchParams };
|
|
282
276
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/Router.tsx","../src/Outlet.tsx","../src/hooks/useNavigate.ts","../src/hooks/useLocation.ts","../src/hooks/
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/Router.tsx","../src/Outlet.tsx","../src/hooks/useNavigate.ts","../src/hooks/useLocation.ts","../src/hooks/useSearchParams.ts","../src/hooks/useBlocker.ts","../src/hooks/useRouteParams.ts","../src/hooks/useRouteState.ts","../src/hooks/useRouteData.ts","../src/hooks/useIsPending.ts","../src/core/RouterAdapter.ts"],"mappings":";;;;cAGM,6BAAA;;AAFwD;;;;;;;KAgBlD,uBAAA;EAAA,CACT,6BAAA,UA4Bc;EA1Bf,IAAA,WAwBI;EAtBJ,QAAA,GAAW,uBAAA;EAiCE;;;;;;EA1Bb,KAAA;EAMA;;;;;EAAA,eAAA,YASI;EAHJ,MAAA,IAAU,IAAA,EAAM,UAAA,CAAW,MAAA,+BAKrB;EAHN,SAAA,GACI,aAAA;IACE,IAAA;IACA,MAAA,GAAS,MAAA;IACT,KAAA;IACA,QAAA,IACE,KAAA,cAAmB,IAAA,2BAChB,OAAA;IACL,YAAA,IAAgB,KAAA,cAAmB,IAAA;IACnC,UAAA;IACA,IAAA;EAAA,KAEF,SAAA;AAAA;;;;KAmBM,YAAA;EAMV,oCAJA,KAAA,EAAO,uBAAA,EAIC;EAFR,MAAA,EAAQ,MAAA,kBAQsB;EAN9B,QAAA;AAAA;;AAcF;;KARY,oBAAA,GAAuB,YAAA;EAUH,6DAR9B,IAAA;AAAA;;;;KAMU,cAAA;EAUe,4DARzB,OAAA,WAAkB,YAAA,WAQO;EANzB,YAAA;AAAA;;;;KAMU,eAAA;EAYQ,uDAVlB,OAAA,YAUkB;EARlB,KAAA,YAUA;EARA,IAAA;AAAA;;AAmBF;;KAbY,QAAA;EACV,QAAA;EACA,MAAA;EACA,IAAA;AAAA;;;;AAqBF;;;;KAXY,kBAAA,IACV,KAAA,EAAO,aAAA,EACP,IAAA,EAAM,cAAA;;;;ACrGR;;;KD8GY,YAAA;;;KC9GA,WAAA;EACV,MAAA,EAAQ,eAAA;ED5BoC;;;;AAc9C;;;ECsBE,UAAA,GAAa,kBAAA;EDjBF;;;;;;ECwBX,QAAA,GAAW,YAAA;AAAA;AAAA,iBAYG,MAAA,CAAA;EACd,MAAA,EAAQ,WAAA;EACR,UAAA;EACA;AAAA,GACC,WAAA,GAAc,SAAA;;;;;;AD7D6C;iBEM9C,MAAA,CAAA,GAAU,SAAA;;;;;;iBCAV,WAAA,CAAA,IAAgB,EAAA,UAAY,OAAA,GAAU,eAAA;;;;;;iBCAtC,WAAA,CAAA,GAAe,QAAA;;;KCJ1B,eAAA,IACH,MAAA,EACI,eAAA,GACA,MAAA,qBACE,IAAA,EAAM,eAAA,KAAoB,eAAA,GAAkB,MAAA;;;;iBAMpC,eAAA,CAAA,IAAoB,eAAA,EAAiB,eAAA;;;KCVzC,iBAAA;;;;ANFkD;EMO5D,WAAA;AAAA;;;ANSF;;;;;;;;;;;;;;;;;;;;;;;;;iBMqBgB,UAAA,CAAW,OAAA,EAAS,iBAAA;;;;;;ANrC0B;;;;;AAgB9D;;;;;;;;;;;;iBOSgB,cAAA,WACJ,4BAAA,SAER,MAAA,oCAAA,CAIF,KAAA,EAAO,CAAA,GAAI,kBAAA,CAAmB,CAAA;;;;;;APhC8B;;;;;AAgB9D;;;;;;;;;;;;;iBQUgB,aAAA,WACJ,4BAAA,SAER,MAAA,oCAAA,CAIF,KAAA,EAAO,CAAA,GAAI,iBAAA,CAAkB,CAAA;;;;;;ARjC+B;;;;;AAgB9D;;;;;;;;;;;;;;;;iBSagB,YAAA,WACJ,4BAAA,SAER,MAAA,oCAAA,CAIF,KAAA,EAAO,CAAA,GAAI,gBAAA,CAAiB,CAAA;;;;;;iBC/Bd,YAAA,CAAA;;;;;;AVL8C;KWSlD,aAAA;wBAEV,GAAA,EAAK,GAAA,EXTuC;EWW5C,GAAA,UXGiC;EWDjC,KAAA,WXEC;EWAD,IAAA;AAAA"}
|
package/dist/index.mjs
CHANGED
|
@@ -417,21 +417,21 @@ function createAdapter(fallback) {
|
|
|
417
417
|
|
|
418
418
|
//#endregion
|
|
419
419
|
//#region src/Router.tsx
|
|
420
|
-
const noopSubscribe = () => () => {};
|
|
421
|
-
const getServerSnapshot = () => null;
|
|
422
420
|
/**
|
|
423
|
-
*
|
|
424
|
-
* This value means to use the `initialEntry` from `useSyncExternalStore` instead.
|
|
421
|
+
* Special value returned as server snapshot during SSR/hydration.
|
|
425
422
|
*/
|
|
426
|
-
const
|
|
423
|
+
const serverSnapshotSymbol = Symbol();
|
|
424
|
+
const noopSubscribe = () => () => {};
|
|
425
|
+
const getServerSnapshot = () => serverSnapshotSymbol;
|
|
427
426
|
function Router({ routes: inputRoutes, onNavigate, fallback = "none" }) {
|
|
428
427
|
const routes = internalRoutes(inputRoutes);
|
|
429
428
|
const adapter = useMemo(() => createAdapter(fallback), [fallback]);
|
|
430
429
|
const [blockerRegistry] = useState(() => createBlockerRegistry());
|
|
431
430
|
const initialEntry = useSyncExternalStore(noopSubscribe, useCallback(() => adapter.getSnapshot(), [adapter]), getServerSnapshot);
|
|
432
431
|
const [isPending, startTransition] = useTransition();
|
|
433
|
-
const [locationEntryInternal, setLocationEntry] = useState(
|
|
434
|
-
const locationEntry = locationEntryInternal ===
|
|
432
|
+
const [locationEntryInternal, setLocationEntry] = useState(initialEntry);
|
|
433
|
+
const locationEntry = locationEntryInternal === serverSnapshotSymbol ? null : locationEntryInternal;
|
|
434
|
+
if (locationEntryInternal === serverSnapshotSymbol && initialEntry !== serverSnapshotSymbol) setLocationEntry(initialEntry);
|
|
435
435
|
useEffect(() => {
|
|
436
436
|
return adapter.subscribe(() => {
|
|
437
437
|
startTransition(() => {
|
|
@@ -649,25 +649,6 @@ function useLocation() {
|
|
|
649
649
|
}, [url]);
|
|
650
650
|
}
|
|
651
651
|
|
|
652
|
-
//#endregion
|
|
653
|
-
//#region src/hooks/useLocationSSR.ts
|
|
654
|
-
/**
|
|
655
|
-
* Returns the current location object, or `null` when the URL is not available (e.g. during SSR).
|
|
656
|
-
*/
|
|
657
|
-
function useLocationSSR() {
|
|
658
|
-
const context = useContext(RouterContext);
|
|
659
|
-
if (!context) throw new Error("useLocationSSR must be used within a Router");
|
|
660
|
-
const { url } = context;
|
|
661
|
-
return useMemo(() => {
|
|
662
|
-
if (url === null) return null;
|
|
663
|
-
return {
|
|
664
|
-
pathname: url.pathname,
|
|
665
|
-
search: url.search,
|
|
666
|
-
hash: url.hash
|
|
667
|
-
};
|
|
668
|
-
}, [url]);
|
|
669
|
-
}
|
|
670
|
-
|
|
671
652
|
//#endregion
|
|
672
653
|
//#region src/hooks/useSearchParams.ts
|
|
673
654
|
/**
|
|
@@ -855,5 +836,5 @@ function useIsPending() {
|
|
|
855
836
|
}
|
|
856
837
|
|
|
857
838
|
//#endregion
|
|
858
|
-
export { Outlet, Router, route, routeState, useBlocker, useIsPending, useLocation,
|
|
839
|
+
export { Outlet, Router, route, routeState, useBlocker, useIsPending, useLocation, useNavigate, useRouteData, useRouteParams, useRouteState, useSearchParams };
|
|
859
840
|
//# sourceMappingURL=index.mjs.map
|