@funstack/router 0.0.8 → 0.0.9
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/docs/ApiComponentsPage.tsx +10 -9
- package/dist/docs/ExamplesPage.tsx +445 -0
- package/dist/docs/GettingStartedPage.tsx +10 -33
- package/dist/docs/LearnNavigationApiPage.tsx +1 -4
- package/dist/docs/LearnNestedRoutesPage.tsx +8 -4
- package/dist/docs/LearnRscPage.tsx +75 -105
- package/dist/docs/LearnSsgPage.tsx +133 -0
- package/dist/docs/{LearnSsrPage.tsx → LearnSsrBasicPage.tsx} +38 -94
- package/dist/docs/LearnSsrWithLoadersPage.tsx +141 -0
- package/dist/docs/LearnTypeSafetyPage.tsx +16 -20
- package/dist/docs/index.md +4 -1
- package/dist/index.d.mts +36 -8
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +36 -8
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -72,19 +72,20 @@ export function ApiComponentsPage() {
|
|
|
72
72
|
</tr>
|
|
73
73
|
<tr>
|
|
74
74
|
<td>
|
|
75
|
-
<code>
|
|
75
|
+
<code>ssr</code>
|
|
76
76
|
</td>
|
|
77
77
|
<td>
|
|
78
|
-
<code>
|
|
78
|
+
<code>SSRConfig</code>
|
|
79
79
|
</td>
|
|
80
80
|
<td>
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
81
|
+
SSR configuration for route matching during server-side
|
|
82
|
+
rendering. Accepts an object with <code>path</code> (the
|
|
83
|
+
pathname to match against) and an optional{" "}
|
|
84
|
+
<code>runLoaders</code> boolean (defaults to <code>false</code>
|
|
85
|
+
). When <code>runLoaders</code> is <code>false</code>, routes
|
|
86
|
+
with loaders are skipped during SSR. Once the client hydrates,
|
|
87
|
+
the real URL from the Navigation API takes over. See the{" "}
|
|
88
|
+
<a href="/learn/ssr">SSR guide</a> for details.
|
|
88
89
|
</td>
|
|
89
90
|
</tr>
|
|
90
91
|
</tbody>
|
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
import { CodeBlock } from "../components/CodeBlock.js";
|
|
2
|
+
|
|
3
|
+
export function ExamplesPage() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="page docs-page">
|
|
6
|
+
<h1>Examples</h1>
|
|
7
|
+
|
|
8
|
+
<section>
|
|
9
|
+
<h2>Basic Routing</h2>
|
|
10
|
+
<p>A simple example with home and about pages:</p>
|
|
11
|
+
<CodeBlock language="tsx">{`import { Router, route, Outlet, useNavigate } from "@funstack/router";
|
|
12
|
+
|
|
13
|
+
function Home() {
|
|
14
|
+
return <h1>Home Page</h1>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function About() {
|
|
18
|
+
return <h1>About Page</h1>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function Layout() {
|
|
22
|
+
const navigate = useNavigate();
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div>
|
|
26
|
+
<nav>
|
|
27
|
+
<button onClick={() => navigate("/")}>Home</button>
|
|
28
|
+
<button onClick={() => navigate("/about")}>About</button>
|
|
29
|
+
</nav>
|
|
30
|
+
<Outlet />
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const routes = [
|
|
36
|
+
route({
|
|
37
|
+
path: "/",
|
|
38
|
+
component: Layout,
|
|
39
|
+
children: [
|
|
40
|
+
route({ path: "/", component: Home }),
|
|
41
|
+
route({ path: "/about", component: About }),
|
|
42
|
+
],
|
|
43
|
+
}),
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
function App() {
|
|
47
|
+
return <Router routes={routes} />;
|
|
48
|
+
}`}</CodeBlock>
|
|
49
|
+
</section>
|
|
50
|
+
|
|
51
|
+
<section>
|
|
52
|
+
<h2>Dynamic Routes with Params</h2>
|
|
53
|
+
<p>
|
|
54
|
+
Handle dynamic URL segments with parameters. Components receive params
|
|
55
|
+
via props:
|
|
56
|
+
</p>
|
|
57
|
+
<CodeBlock language="tsx">{`import { route } from "@funstack/router";
|
|
58
|
+
|
|
59
|
+
function UserProfile({ params }: { params: { userId: string } }) {
|
|
60
|
+
return <h1>Viewing user: {params.userId}</h1>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function PostDetail({
|
|
64
|
+
params,
|
|
65
|
+
}: {
|
|
66
|
+
params: { userId: string; postId: string };
|
|
67
|
+
}) {
|
|
68
|
+
return (
|
|
69
|
+
<div>
|
|
70
|
+
<h1>Post {params.postId}</h1>
|
|
71
|
+
<p>By user {params.userId}</p>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const routes = [
|
|
77
|
+
route({
|
|
78
|
+
path: "/users/:userId",
|
|
79
|
+
component: UserProfile,
|
|
80
|
+
}),
|
|
81
|
+
route({
|
|
82
|
+
path: "/users/:userId/posts/:postId",
|
|
83
|
+
component: PostDetail,
|
|
84
|
+
}),
|
|
85
|
+
];`}</CodeBlock>
|
|
86
|
+
</section>
|
|
87
|
+
|
|
88
|
+
<section>
|
|
89
|
+
<h2>Nested Routes</h2>
|
|
90
|
+
<p>Create complex layouts with nested routing:</p>
|
|
91
|
+
<CodeBlock language="tsx">{`import { route, Outlet } from "@funstack/router";
|
|
92
|
+
|
|
93
|
+
function Dashboard() {
|
|
94
|
+
return (
|
|
95
|
+
<div className="dashboard">
|
|
96
|
+
<aside>
|
|
97
|
+
<nav>Dashboard Menu</nav>
|
|
98
|
+
</aside>
|
|
99
|
+
<main>
|
|
100
|
+
<Outlet />
|
|
101
|
+
</main>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function DashboardHome() {
|
|
107
|
+
return <h2>Dashboard Overview</h2>;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function DashboardSettings() {
|
|
111
|
+
return <h2>Settings</h2>;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function DashboardProfile() {
|
|
115
|
+
return <h2>Your Profile</h2>;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const dashboardRoutes = route({
|
|
119
|
+
path: "/dashboard",
|
|
120
|
+
component: Dashboard,
|
|
121
|
+
children: [
|
|
122
|
+
route({ path: "/", component: DashboardHome }),
|
|
123
|
+
route({ path: "/settings", component: DashboardSettings }),
|
|
124
|
+
route({ path: "/profile", component: DashboardProfile }),
|
|
125
|
+
],
|
|
126
|
+
});`}</CodeBlock>
|
|
127
|
+
</section>
|
|
128
|
+
|
|
129
|
+
<section>
|
|
130
|
+
<h2>Data Loading</h2>
|
|
131
|
+
<p>
|
|
132
|
+
Load data with loaders. When a loader returns a Promise, the component
|
|
133
|
+
receives that Promise and uses React's <code>use</code> hook to unwrap
|
|
134
|
+
it. Use <code><Suspense></code> within your pages or layouts to
|
|
135
|
+
handle loading states.
|
|
136
|
+
</p>
|
|
137
|
+
<CodeBlock language="tsx">{`import { use, Suspense } from "react";
|
|
138
|
+
import { route } from "@funstack/router";
|
|
139
|
+
|
|
140
|
+
interface Post {
|
|
141
|
+
id: number;
|
|
142
|
+
title: string;
|
|
143
|
+
body: string;
|
|
144
|
+
userId: number;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// When the loader returns a Promise, the component receives that Promise.
|
|
148
|
+
// Use React's \`use\` hook to unwrap the Promise.
|
|
149
|
+
function UserPostsContent({
|
|
150
|
+
data,
|
|
151
|
+
params,
|
|
152
|
+
}: {
|
|
153
|
+
data: Promise<Post[]>;
|
|
154
|
+
params: { userId: string };
|
|
155
|
+
}) {
|
|
156
|
+
const posts = use(data);
|
|
157
|
+
return (
|
|
158
|
+
<div>
|
|
159
|
+
<h2>Posts by user {params.userId}</h2>
|
|
160
|
+
<ul>
|
|
161
|
+
{posts.map((post) => (
|
|
162
|
+
<li key={post.id}>{post.title}</li>
|
|
163
|
+
))}
|
|
164
|
+
</ul>
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Wrap the content with Suspense at the page level
|
|
170
|
+
function UserPosts(props: {
|
|
171
|
+
data: Promise<Post[]>;
|
|
172
|
+
params: { userId: string };
|
|
173
|
+
}) {
|
|
174
|
+
return (
|
|
175
|
+
<Suspense fallback={<div>Loading posts...</div>}>
|
|
176
|
+
<UserPostsContent {...props} />
|
|
177
|
+
</Suspense>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const userPostsRoute = route({
|
|
182
|
+
path: "/users/:userId/posts",
|
|
183
|
+
component: UserPosts,
|
|
184
|
+
loader: async ({ params }): Promise<Post[]> => {
|
|
185
|
+
const response = await fetch(
|
|
186
|
+
\`https://jsonplaceholder.typicode.com/posts?userId=\${params.userId}\`
|
|
187
|
+
);
|
|
188
|
+
return response.json();
|
|
189
|
+
},
|
|
190
|
+
});`}</CodeBlock>
|
|
191
|
+
</section>
|
|
192
|
+
|
|
193
|
+
<section>
|
|
194
|
+
<h2>Form Submissions</h2>
|
|
195
|
+
<p>
|
|
196
|
+
Handle form POST submissions with route actions. The action receives
|
|
197
|
+
the form data, and its return value flows to the loader via{" "}
|
|
198
|
+
<code>actionResult</code>. Native <code><form></code> elements
|
|
199
|
+
work out of the box — no wrapper component needed.
|
|
200
|
+
</p>
|
|
201
|
+
<CodeBlock language="tsx">{`import { route, type RouteComponentPropsOf } from "@funstack/router";
|
|
202
|
+
|
|
203
|
+
// Define a route with both action and loader
|
|
204
|
+
const editUserRoute = route({
|
|
205
|
+
id: "editUser",
|
|
206
|
+
path: "/users/:userId/edit",
|
|
207
|
+
action: async ({ request, params, signal }) => {
|
|
208
|
+
const formData = await request.formData();
|
|
209
|
+
const response = await fetch(\`/api/users/\${params.userId}\`, {
|
|
210
|
+
method: "PUT",
|
|
211
|
+
body: formData,
|
|
212
|
+
signal,
|
|
213
|
+
});
|
|
214
|
+
return response.json() as Promise<{ success: boolean; error?: string }>;
|
|
215
|
+
},
|
|
216
|
+
loader: async ({ params, signal, actionResult }) => {
|
|
217
|
+
const user = await fetchUser(params.userId, signal);
|
|
218
|
+
return {
|
|
219
|
+
user,
|
|
220
|
+
// actionResult is undefined on normal navigation,
|
|
221
|
+
// contains the action's return value after form submission
|
|
222
|
+
updateResult: actionResult ?? null,
|
|
223
|
+
};
|
|
224
|
+
},
|
|
225
|
+
component: EditUserPage,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Component receives data from the loader (which includes the action result)
|
|
229
|
+
function EditUserPage({ data, isPending }: RouteComponentPropsOf<typeof editUserRoute>) {
|
|
230
|
+
return (
|
|
231
|
+
<form method="post">
|
|
232
|
+
{data.updateResult?.error && (
|
|
233
|
+
<p className="error">{data.updateResult.error}</p>
|
|
234
|
+
)}
|
|
235
|
+
{data.updateResult?.success && (
|
|
236
|
+
<p className="success">User updated successfully!</p>
|
|
237
|
+
)}
|
|
238
|
+
<input name="name" defaultValue={data.user.name} />
|
|
239
|
+
<input name="email" defaultValue={data.user.email} />
|
|
240
|
+
<button type="submit" disabled={isPending}>
|
|
241
|
+
{isPending ? "Saving..." : "Save"}
|
|
242
|
+
</button>
|
|
243
|
+
</form>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Route with action only (pure side effect, no data for component)
|
|
248
|
+
const deleteRoute = route({
|
|
249
|
+
path: "/users/:userId/delete",
|
|
250
|
+
action: async ({ params, signal }) => {
|
|
251
|
+
await fetch(\`/api/users/\${params.userId}\`, {
|
|
252
|
+
method: "DELETE",
|
|
253
|
+
signal,
|
|
254
|
+
});
|
|
255
|
+
},
|
|
256
|
+
component: DeleteConfirmation,
|
|
257
|
+
});`}</CodeBlock>
|
|
258
|
+
<p>Key behaviors:</p>
|
|
259
|
+
<ul>
|
|
260
|
+
<li>
|
|
261
|
+
Actions handle POST form submissions; loaders handle GET navigations
|
|
262
|
+
</li>
|
|
263
|
+
<li>Action results are never cached — each submission runs fresh</li>
|
|
264
|
+
<li>
|
|
265
|
+
After an action completes, all matched loaders are revalidated
|
|
266
|
+
</li>
|
|
267
|
+
<li>
|
|
268
|
+
POST submissions to routes without an action are not intercepted
|
|
269
|
+
(browser handles normally)
|
|
270
|
+
</li>
|
|
271
|
+
<li>
|
|
272
|
+
The deepest matched route with an action handles the submission
|
|
273
|
+
</li>
|
|
274
|
+
</ul>
|
|
275
|
+
</section>
|
|
276
|
+
|
|
277
|
+
<section>
|
|
278
|
+
<h2>Search Parameters</h2>
|
|
279
|
+
<p>Work with URL query parameters:</p>
|
|
280
|
+
<CodeBlock language="tsx">{`import { useSearchParams } from "@funstack/router";
|
|
281
|
+
|
|
282
|
+
function ProductList() {
|
|
283
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
284
|
+
|
|
285
|
+
const category = searchParams.get("category") || "all";
|
|
286
|
+
const sortBy = searchParams.get("sort") || "name";
|
|
287
|
+
|
|
288
|
+
const handleCategoryChange = (newCategory: string) => {
|
|
289
|
+
setSearchParams({
|
|
290
|
+
category: newCategory,
|
|
291
|
+
sort: sortBy,
|
|
292
|
+
});
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const handleSortChange = (newSort: string) => {
|
|
296
|
+
setSearchParams({
|
|
297
|
+
category,
|
|
298
|
+
sort: newSort,
|
|
299
|
+
});
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
return (
|
|
303
|
+
<div>
|
|
304
|
+
<select value={category} onChange={(e) => handleCategoryChange(e.target.value)}>
|
|
305
|
+
<option value="all">All Categories</option>
|
|
306
|
+
<option value="electronics">Electronics</option>
|
|
307
|
+
<option value="clothing">Clothing</option>
|
|
308
|
+
</select>
|
|
309
|
+
|
|
310
|
+
<select value={sortBy} onChange={(e) => handleSortChange(e.target.value)}>
|
|
311
|
+
<option value="name">Sort by Name</option>
|
|
312
|
+
<option value="price">Sort by Price</option>
|
|
313
|
+
</select>
|
|
314
|
+
|
|
315
|
+
<p>Showing {category} products, sorted by {sortBy}</p>
|
|
316
|
+
</div>
|
|
317
|
+
);
|
|
318
|
+
}`}</CodeBlock>
|
|
319
|
+
</section>
|
|
320
|
+
|
|
321
|
+
<section>
|
|
322
|
+
<h2>Pathless Routes</h2>
|
|
323
|
+
<p>
|
|
324
|
+
Routes without a <code>path</code> are called "pathless routes". They
|
|
325
|
+
always match and don't consume any pathname, making them ideal for
|
|
326
|
+
layout wrappers that don't affect the URL structure.
|
|
327
|
+
</p>
|
|
328
|
+
<CodeBlock language="tsx">{`import { route, Outlet } from "@funstack/router";
|
|
329
|
+
|
|
330
|
+
// A layout wrapper that provides authentication context
|
|
331
|
+
function AuthLayout() {
|
|
332
|
+
return (
|
|
333
|
+
<AuthProvider>
|
|
334
|
+
<Outlet />
|
|
335
|
+
</AuthProvider>
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// A layout wrapper that provides a sidebar
|
|
340
|
+
function DashboardLayout() {
|
|
341
|
+
return (
|
|
342
|
+
<div className="dashboard">
|
|
343
|
+
<Sidebar />
|
|
344
|
+
<main>
|
|
345
|
+
<Outlet />
|
|
346
|
+
</main>
|
|
347
|
+
</div>
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const routes = [
|
|
352
|
+
route({
|
|
353
|
+
path: "/",
|
|
354
|
+
component: RootLayout,
|
|
355
|
+
children: [
|
|
356
|
+
route({ path: "/", component: HomePage }),
|
|
357
|
+
route({ path: "/about", component: AboutPage }),
|
|
358
|
+
// Pathless route wraps authenticated pages
|
|
359
|
+
route({
|
|
360
|
+
component: AuthLayout,
|
|
361
|
+
children: [
|
|
362
|
+
// Another pathless route for dashboard layout
|
|
363
|
+
route({
|
|
364
|
+
component: DashboardLayout,
|
|
365
|
+
children: [
|
|
366
|
+
route({ path: "/dashboard", component: DashboardHome }),
|
|
367
|
+
route({ path: "/dashboard/settings", component: Settings }),
|
|
368
|
+
route({ path: "/dashboard/profile", component: Profile }),
|
|
369
|
+
],
|
|
370
|
+
}),
|
|
371
|
+
],
|
|
372
|
+
}),
|
|
373
|
+
],
|
|
374
|
+
}),
|
|
375
|
+
];`}</CodeBlock>
|
|
376
|
+
<p>Key behaviors of pathless routes:</p>
|
|
377
|
+
<ul>
|
|
378
|
+
<li>
|
|
379
|
+
<strong>Always match</strong> - They don't participate in path
|
|
380
|
+
matching
|
|
381
|
+
</li>
|
|
382
|
+
<li>
|
|
383
|
+
<strong>Consume no pathname</strong> - Children receive the full
|
|
384
|
+
remaining pathname
|
|
385
|
+
</li>
|
|
386
|
+
<li>
|
|
387
|
+
<strong>No params</strong> - The <code>params</code> prop is always
|
|
388
|
+
an empty object
|
|
389
|
+
</li>
|
|
390
|
+
<li>
|
|
391
|
+
<strong>Can have loaders</strong> - Pathless routes can still load
|
|
392
|
+
data
|
|
393
|
+
</li>
|
|
394
|
+
</ul>
|
|
395
|
+
<h3>Catch-All Routes</h3>
|
|
396
|
+
<p>
|
|
397
|
+
Use <code>path: "/*"</code> at the end of a route list to catch any
|
|
398
|
+
unmatched paths:
|
|
399
|
+
</p>
|
|
400
|
+
<CodeBlock language="tsx">{`const routes = [
|
|
401
|
+
route({ path: "/", component: HomePage }),
|
|
402
|
+
route({ path: "/about", component: AboutPage }),
|
|
403
|
+
// Catch-all: matches any unmatched path
|
|
404
|
+
route({ path: "/*", component: NotFoundPage }),
|
|
405
|
+
];`}</CodeBlock>
|
|
406
|
+
</section>
|
|
407
|
+
|
|
408
|
+
<section>
|
|
409
|
+
<h2>Navigation Callback</h2>
|
|
410
|
+
<p>
|
|
411
|
+
React to navigation events. The callback receives the{" "}
|
|
412
|
+
<code>NavigateEvent</code> from the Navigation API and an info object
|
|
413
|
+
containing matched routes and whether the navigation will be
|
|
414
|
+
intercepted:
|
|
415
|
+
</p>
|
|
416
|
+
<CodeBlock language="tsx">{`import { Router, route, type OnNavigateCallback } from "@funstack/router";
|
|
417
|
+
|
|
418
|
+
function App() {
|
|
419
|
+
const handleNavigate: OnNavigateCallback = (event, info) => {
|
|
420
|
+
// Track page views
|
|
421
|
+
const url = new URL(event.destination.url);
|
|
422
|
+
analytics.track("page_view", {
|
|
423
|
+
path: url.pathname,
|
|
424
|
+
search: url.search,
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
// info.matches contains the matched routes (or null if no match)
|
|
428
|
+
// info.intercepting indicates if the router will handle this navigation
|
|
429
|
+
console.log("Matched routes:", info.matches);
|
|
430
|
+
console.log("Intercepting:", info.intercepting);
|
|
431
|
+
|
|
432
|
+
// You can call event.preventDefault() to cancel the navigation
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
return (
|
|
436
|
+
<Router
|
|
437
|
+
routes={routes}
|
|
438
|
+
onNavigate={handleNavigate}
|
|
439
|
+
/>
|
|
440
|
+
);
|
|
441
|
+
}`}</CodeBlock>
|
|
442
|
+
</section>
|
|
443
|
+
</div>
|
|
444
|
+
);
|
|
445
|
+
}
|
|
@@ -26,8 +26,13 @@ yarn add @funstack/router`}</CodeBlock>
|
|
|
26
26
|
<CodeBlock language="bash">{`npx funstack-router-skill-installer`}</CodeBlock>
|
|
27
27
|
<p>
|
|
28
28
|
The installer will guide you through setting up the skill for your
|
|
29
|
-
preferred AI agent.
|
|
29
|
+
preferred AI agent. Alternatively, if you prefer{" "}
|
|
30
|
+
<a href="https://skills.sh/" target="_blank">
|
|
31
|
+
npx skills
|
|
32
|
+
</a>
|
|
33
|
+
, you can install it with:
|
|
30
34
|
</p>
|
|
35
|
+
<CodeBlock language="bash">{`npx skills add uhyo/funstack-router`}</CodeBlock>
|
|
31
36
|
</section>
|
|
32
37
|
|
|
33
38
|
<section>
|
|
@@ -113,34 +118,6 @@ const routes = [
|
|
|
113
118
|
component: UserProfile,
|
|
114
119
|
}),
|
|
115
120
|
];`}</CodeBlock>
|
|
116
|
-
<p>
|
|
117
|
-
Alternatively, you can use the <code>useParams</code> hook to access
|
|
118
|
-
parameters:
|
|
119
|
-
</p>
|
|
120
|
-
<CodeBlock language="tsx">{`import { useParams } from "@funstack/router";
|
|
121
|
-
|
|
122
|
-
function UserProfile() {
|
|
123
|
-
const params = useParams<{ userId: string }>();
|
|
124
|
-
return <h1>User: {params.userId}</h1>;
|
|
125
|
-
}`}</CodeBlock>
|
|
126
|
-
</section>
|
|
127
|
-
|
|
128
|
-
<section>
|
|
129
|
-
<h2>Programmatic Navigation</h2>
|
|
130
|
-
<p>
|
|
131
|
-
Use the <code>useNavigate</code> hook for programmatic navigation:
|
|
132
|
-
</p>
|
|
133
|
-
<CodeBlock language="tsx">{`import { useNavigate } from "@funstack/router";
|
|
134
|
-
|
|
135
|
-
function MyComponent() {
|
|
136
|
-
const navigate = useNavigate();
|
|
137
|
-
|
|
138
|
-
const handleClick = () => {
|
|
139
|
-
navigate("/about");
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
return <button onClick={handleClick}>Go to About</button>;
|
|
143
|
-
}`}</CodeBlock>
|
|
144
121
|
</section>
|
|
145
122
|
|
|
146
123
|
<section>
|
|
@@ -164,21 +141,21 @@ function UserProfilePage({
|
|
|
164
141
|
data: Promise<User>;
|
|
165
142
|
params: { userId: string };
|
|
166
143
|
}) {
|
|
167
|
-
const user = use(data);
|
|
168
144
|
return (
|
|
169
145
|
<Suspense fallback={<div>Loading...</div>}>
|
|
170
|
-
<UserProfile
|
|
146
|
+
<UserProfile data={data} params={params} />
|
|
171
147
|
</Suspense>
|
|
172
148
|
);
|
|
173
149
|
}
|
|
174
150
|
|
|
175
151
|
function UserProfile({
|
|
176
|
-
|
|
152
|
+
data,
|
|
177
153
|
params,
|
|
178
154
|
}: {
|
|
179
|
-
|
|
155
|
+
data: Promise<User>;
|
|
180
156
|
params: { userId: string };
|
|
181
157
|
}) {
|
|
158
|
+
const user = use(data);
|
|
182
159
|
return (
|
|
183
160
|
<div>
|
|
184
161
|
<h1>{user.name}</h1>
|
|
@@ -155,15 +155,12 @@ function Navigation() {
|
|
|
155
155
|
<p>
|
|
156
156
|
While FUNSTACK Router handles navigation for you, you can interact
|
|
157
157
|
directly with the Navigation API when needed. This is useful for
|
|
158
|
-
features like
|
|
158
|
+
features like analytics tracking.
|
|
159
159
|
</p>
|
|
160
160
|
<CodeBlock language="tsx">{`import { useEffect } from "react";
|
|
161
161
|
|
|
162
162
|
function App() {
|
|
163
163
|
useEffect(() => {
|
|
164
|
-
const navigation = window.navigation;
|
|
165
|
-
if (!navigation) return;
|
|
166
|
-
|
|
167
164
|
const controller = new AbortController();
|
|
168
165
|
|
|
169
166
|
// Listen for successful navigation completion
|
|
@@ -338,7 +338,7 @@ const routes = [
|
|
|
338
338
|
<CodeBlock language="tsx">{`import { use, Suspense } from "react";
|
|
339
339
|
import { route, Outlet, useRouteData } from "@funstack/router";
|
|
340
340
|
|
|
341
|
-
// Define the parent route with a loader
|
|
341
|
+
// Define the parent route with a loader and child routes
|
|
342
342
|
const teamRoute = route({
|
|
343
343
|
id: "team",
|
|
344
344
|
path: "/teams/:teamId",
|
|
@@ -347,9 +347,14 @@ const teamRoute = route({
|
|
|
347
347
|
const response = await fetch(\`/api/teams/\${params.teamId}\`);
|
|
348
348
|
return response.json();
|
|
349
349
|
},
|
|
350
|
+
children: [
|
|
351
|
+
route({ path: "/", component: TeamOverview }),
|
|
352
|
+
route({ path: "/members", component: TeamMembers }),
|
|
353
|
+
route({ path: "/settings", component: TeamSettings }),
|
|
354
|
+
],
|
|
350
355
|
});
|
|
351
356
|
|
|
352
|
-
// Parent layout loads team data once
|
|
357
|
+
// Parent layout loads team data once, child routes render in <Outlet />
|
|
353
358
|
function TeamLayoutContent({
|
|
354
359
|
data,
|
|
355
360
|
}: {
|
|
@@ -439,8 +444,7 @@ function TeamLayout(props: {
|
|
|
439
444
|
Pathless routes also play a key role in server-side rendering. During
|
|
440
445
|
SSR, only pathless routes render (since no URL is available on the
|
|
441
446
|
server), making them ideal for defining the app shell. See the{" "}
|
|
442
|
-
<a href="/learn/
|
|
443
|
-
for details.
|
|
447
|
+
<a href="/learn/ssr">Server-Side Rendering</a> page for details.
|
|
444
448
|
</p>
|
|
445
449
|
</section>
|
|
446
450
|
|