@funstack/router 0.0.6 → 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 +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,601 @@
|
|
|
1
|
+
import { CodeBlock } from "../components/CodeBlock.js";
|
|
2
|
+
|
|
3
|
+
export function LearnNestedRoutesPage() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="learn-content">
|
|
6
|
+
<h2>Nested Routes</h2>
|
|
7
|
+
|
|
8
|
+
<p className="page-intro">
|
|
9
|
+
<b>Nested routes</b> let you build complex page layouts where parts of
|
|
10
|
+
the UI persist across navigation while other parts change. Think of a
|
|
11
|
+
dashboard with a sidebar that stays in place while the main content area
|
|
12
|
+
updates—that's nested routing in action.
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
<section>
|
|
16
|
+
<h3>Why Nested Routes?</h3>
|
|
17
|
+
<p>
|
|
18
|
+
Consider a typical dashboard application. You have a header, a
|
|
19
|
+
sidebar, and a main content area. When users navigate between
|
|
20
|
+
different dashboard pages, the header and sidebar should remain
|
|
21
|
+
visible while only the main content changes.
|
|
22
|
+
</p>
|
|
23
|
+
<p>Without nested routes, you'd have two options:</p>
|
|
24
|
+
<ul>
|
|
25
|
+
<li>
|
|
26
|
+
<strong>Duplicate the layout</strong> in every page component,
|
|
27
|
+
leading to repetition and maintenance headaches
|
|
28
|
+
</li>
|
|
29
|
+
<li>
|
|
30
|
+
<strong>Use conditional rendering</strong> based on the current
|
|
31
|
+
route, which quickly becomes complex and hard to manage
|
|
32
|
+
</li>
|
|
33
|
+
</ul>
|
|
34
|
+
<p>
|
|
35
|
+
Nested routes solve this elegantly by letting you compose layouts
|
|
36
|
+
hierarchically. Parent routes define the persistent UI, and child
|
|
37
|
+
routes fill in the changing parts.
|
|
38
|
+
</p>
|
|
39
|
+
</section>
|
|
40
|
+
|
|
41
|
+
<section>
|
|
42
|
+
<h3>The Outlet Component</h3>
|
|
43
|
+
<p>
|
|
44
|
+
The key to nested routing is the <code>{"<Outlet>"}</code> component.
|
|
45
|
+
It acts as a placeholder in a parent route's component where child
|
|
46
|
+
routes will be rendered.
|
|
47
|
+
</p>
|
|
48
|
+
<CodeBlock language="tsx">{`import { Outlet } from "@funstack/router";
|
|
49
|
+
|
|
50
|
+
function DashboardLayout() {
|
|
51
|
+
return (
|
|
52
|
+
<div className="dashboard">
|
|
53
|
+
<aside className="sidebar">
|
|
54
|
+
<nav>
|
|
55
|
+
<a href="/dashboard">Overview</a>
|
|
56
|
+
<a href="/dashboard/analytics">Analytics</a>
|
|
57
|
+
<a href="/dashboard/settings">Settings</a>
|
|
58
|
+
</nav>
|
|
59
|
+
</aside>
|
|
60
|
+
<main className="content">
|
|
61
|
+
{/* Child routes render here */}
|
|
62
|
+
<Outlet />
|
|
63
|
+
</main>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}`}</CodeBlock>
|
|
67
|
+
<p>
|
|
68
|
+
When you navigate to <code>/dashboard/analytics</code>, the{" "}
|
|
69
|
+
<code>DashboardLayout</code> component renders the sidebar, and the{" "}
|
|
70
|
+
<code>{"<Outlet>"}</code> renders the Analytics page component.
|
|
71
|
+
</p>
|
|
72
|
+
</section>
|
|
73
|
+
|
|
74
|
+
<section>
|
|
75
|
+
<h3>Defining Nested Routes</h3>
|
|
76
|
+
<p>
|
|
77
|
+
Nested routes are defined using the <code>children</code> property in
|
|
78
|
+
your route definitions. Each child route's path is{" "}
|
|
79
|
+
<strong>relative to its parent</strong>.
|
|
80
|
+
</p>
|
|
81
|
+
<CodeBlock language="tsx">{`import { route } from "@funstack/router";
|
|
82
|
+
|
|
83
|
+
const routes = [
|
|
84
|
+
route({
|
|
85
|
+
path: "/dashboard",
|
|
86
|
+
component: DashboardLayout,
|
|
87
|
+
children: [
|
|
88
|
+
// Matches "/dashboard" exactly
|
|
89
|
+
route({
|
|
90
|
+
path: "/",
|
|
91
|
+
component: DashboardOverview,
|
|
92
|
+
}),
|
|
93
|
+
// Matches "/dashboard/analytics"
|
|
94
|
+
route({
|
|
95
|
+
path: "/analytics",
|
|
96
|
+
component: AnalyticsPage,
|
|
97
|
+
}),
|
|
98
|
+
// Matches "/dashboard/settings"
|
|
99
|
+
route({
|
|
100
|
+
path: "/settings",
|
|
101
|
+
component: SettingsPage,
|
|
102
|
+
}),
|
|
103
|
+
],
|
|
104
|
+
}),
|
|
105
|
+
];`}</CodeBlock>
|
|
106
|
+
<p>
|
|
107
|
+
Notice how child paths start with <code>/</code> but are relative to
|
|
108
|
+
the parent. The path <code>/analytics</code> under a parent with path{" "}
|
|
109
|
+
<code>/dashboard</code> matches the full URL{" "}
|
|
110
|
+
<code>/dashboard/analytics</code>.
|
|
111
|
+
</p>
|
|
112
|
+
</section>
|
|
113
|
+
|
|
114
|
+
<section>
|
|
115
|
+
<h3>Route Matching Behavior</h3>
|
|
116
|
+
<p>
|
|
117
|
+
Understanding how routes match URLs is crucial for nested routing. The
|
|
118
|
+
router uses different matching strategies depending on whether a route
|
|
119
|
+
has children.
|
|
120
|
+
</p>
|
|
121
|
+
|
|
122
|
+
<h4>Parent Routes: Prefix Matching</h4>
|
|
123
|
+
<p>
|
|
124
|
+
Routes with <code>children</code> use <strong>prefix matching</strong>
|
|
125
|
+
. They match any URL that starts with their path, allowing child
|
|
126
|
+
routes to match the remaining portion.
|
|
127
|
+
</p>
|
|
128
|
+
<CodeBlock language="tsx">{`route({
|
|
129
|
+
path: "/dashboard", // Matches "/dashboard", "/dashboard/settings", etc.
|
|
130
|
+
component: DashboardLayout,
|
|
131
|
+
children: [
|
|
132
|
+
// Children handle the rest of the URL
|
|
133
|
+
],
|
|
134
|
+
})`}</CodeBlock>
|
|
135
|
+
<p>
|
|
136
|
+
When you navigate to <code>/dashboard/settings/profile</code>, the
|
|
137
|
+
parent route <code>/dashboard</code> matches and consumes that portion
|
|
138
|
+
of the URL. The remaining <code>/settings/profile</code> is then
|
|
139
|
+
matched against its children.
|
|
140
|
+
</p>
|
|
141
|
+
|
|
142
|
+
<h4>Leaf Routes: Exact Matching</h4>
|
|
143
|
+
<p>
|
|
144
|
+
Routes without <code>children</code> (leaf routes) use{" "}
|
|
145
|
+
<strong>exact matching</strong> by default. They only match when the
|
|
146
|
+
URL exactly matches their full path.
|
|
147
|
+
</p>
|
|
148
|
+
<CodeBlock language="tsx">{`route({
|
|
149
|
+
path: "/dashboard",
|
|
150
|
+
component: DashboardLayout,
|
|
151
|
+
children: [
|
|
152
|
+
route({
|
|
153
|
+
path: "/settings", // Only matches "/dashboard/settings" exactly
|
|
154
|
+
component: SettingsPage,
|
|
155
|
+
}),
|
|
156
|
+
],
|
|
157
|
+
})`}</CodeBlock>
|
|
158
|
+
<p>
|
|
159
|
+
With this configuration, <code>/dashboard/settings</code> matches, but{" "}
|
|
160
|
+
<code>/dashboard/settings/advanced</code> does not—there's no
|
|
161
|
+
child route to handle <code>/advanced</code>.
|
|
162
|
+
</p>
|
|
163
|
+
|
|
164
|
+
<h4>The Index Route Pattern</h4>
|
|
165
|
+
<p>
|
|
166
|
+
A common pattern is using <code>path: "/"</code> as an index route.
|
|
167
|
+
This matches when the parent's path is matched exactly with no
|
|
168
|
+
additional segments.
|
|
169
|
+
</p>
|
|
170
|
+
<CodeBlock language="tsx">{`route({
|
|
171
|
+
path: "/dashboard",
|
|
172
|
+
component: DashboardLayout,
|
|
173
|
+
children: [
|
|
174
|
+
route({
|
|
175
|
+
path: "/", // Matches "/dashboard" exactly
|
|
176
|
+
component: DashboardHome,
|
|
177
|
+
}),
|
|
178
|
+
route({
|
|
179
|
+
path: "/settings", // Matches "/dashboard/settings"
|
|
180
|
+
component: SettingsPage,
|
|
181
|
+
}),
|
|
182
|
+
],
|
|
183
|
+
})`}</CodeBlock>
|
|
184
|
+
<p>
|
|
185
|
+
Here, navigating to <code>/dashboard</code> renders{" "}
|
|
186
|
+
<code>DashboardLayout</code> with <code>DashboardHome</code> in its
|
|
187
|
+
outlet. Navigating to <code>/dashboard/settings</code> renders{" "}
|
|
188
|
+
<code>DashboardLayout</code> with <code>SettingsPage</code> instead.
|
|
189
|
+
</p>
|
|
190
|
+
|
|
191
|
+
<h4>Forcing Exact Matching</h4>
|
|
192
|
+
<p>
|
|
193
|
+
Sometimes you want a parent route to only match its exact path, not
|
|
194
|
+
act as a prefix. Use the <code>exact</code> option:
|
|
195
|
+
</p>
|
|
196
|
+
<CodeBlock language="tsx">{`route({
|
|
197
|
+
path: "/blog",
|
|
198
|
+
exact: true, // Only matches "/blog", not "/blog/post-1"
|
|
199
|
+
component: BlogIndex,
|
|
200
|
+
children: [
|
|
201
|
+
route({
|
|
202
|
+
path: "/:slug", // This won't match because parent requires exact
|
|
203
|
+
component: BlogPost,
|
|
204
|
+
}),
|
|
205
|
+
],
|
|
206
|
+
})`}</CodeBlock>
|
|
207
|
+
<p>
|
|
208
|
+
With <code>exact: true</code>, the route only matches when the URL is
|
|
209
|
+
exactly <code>/blog</code>. URLs like <code>/blog/post-1</code> won't
|
|
210
|
+
match this route at all. This is rarely needed but useful when you
|
|
211
|
+
want a route to behave as a leaf even though it has children defined.
|
|
212
|
+
</p>
|
|
213
|
+
|
|
214
|
+
<h4>Requiring Children to Match</h4>
|
|
215
|
+
<p>
|
|
216
|
+
By default, parent routes <strong>require</strong> at least one child
|
|
217
|
+
route to match. If no children match, the parent doesn't match
|
|
218
|
+
either—allowing other routes (like a catch-all) to handle the
|
|
219
|
+
URL instead.
|
|
220
|
+
</p>
|
|
221
|
+
<CodeBlock language="tsx">{`const routes = [
|
|
222
|
+
route({
|
|
223
|
+
path: "/dashboard",
|
|
224
|
+
component: DashboardLayout,
|
|
225
|
+
children: [
|
|
226
|
+
route({ path: "/", component: DashboardHome }),
|
|
227
|
+
route({ path: "/settings", component: SettingsPage }),
|
|
228
|
+
],
|
|
229
|
+
}),
|
|
230
|
+
route({
|
|
231
|
+
path: "/*", // Catch-all for unmatched routes
|
|
232
|
+
component: NotFoundPage,
|
|
233
|
+
}),
|
|
234
|
+
];
|
|
235
|
+
|
|
236
|
+
// /dashboard → matches DashboardLayout + DashboardHome
|
|
237
|
+
// /dashboard/settings → matches DashboardLayout + SettingsPage
|
|
238
|
+
// /dashboard/unknown → matches NotFoundPage (not DashboardLayout)`}</CodeBlock>
|
|
239
|
+
<p>
|
|
240
|
+
This behavior ensures that catch-all routes work intuitively. Without
|
|
241
|
+
it, <code>/dashboard/unknown</code> would match the dashboard layout
|
|
242
|
+
with an empty outlet, which is usually not desired.
|
|
243
|
+
</p>
|
|
244
|
+
<p>
|
|
245
|
+
If you want a parent route to match even when no children match, set{" "}
|
|
246
|
+
<code>requireChildren: false</code>. The <code>{"<Outlet>"}</code>{" "}
|
|
247
|
+
will render <code>null</code> in this case.
|
|
248
|
+
</p>
|
|
249
|
+
<CodeBlock language="tsx">{`route({
|
|
250
|
+
path: "/files",
|
|
251
|
+
component: FileExplorer,
|
|
252
|
+
requireChildren: false, // Match even without child matches
|
|
253
|
+
children: [
|
|
254
|
+
route({ path: "/:fileId", component: FileDetails }),
|
|
255
|
+
],
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// /files → matches FileExplorer (outlet is null)
|
|
259
|
+
// /files/123 → matches FileExplorer + FileDetails`}</CodeBlock>
|
|
260
|
+
</section>
|
|
261
|
+
|
|
262
|
+
<section>
|
|
263
|
+
<h3>Multiple Levels of Nesting</h3>
|
|
264
|
+
<p>
|
|
265
|
+
You can nest routes as deeply as your application requires. Each level
|
|
266
|
+
can have its own layout component with an <code>{"<Outlet>"}</code>.
|
|
267
|
+
</p>
|
|
268
|
+
<CodeBlock language="tsx">{`const routes = [
|
|
269
|
+
route({
|
|
270
|
+
path: "/",
|
|
271
|
+
component: RootLayout, // Header, footer
|
|
272
|
+
children: [
|
|
273
|
+
route({
|
|
274
|
+
path: "/",
|
|
275
|
+
component: HomePage,
|
|
276
|
+
}),
|
|
277
|
+
route({
|
|
278
|
+
path: "/dashboard",
|
|
279
|
+
component: DashboardLayout, // Adds sidebar
|
|
280
|
+
children: [
|
|
281
|
+
route({
|
|
282
|
+
path: "/",
|
|
283
|
+
component: DashboardHome,
|
|
284
|
+
}),
|
|
285
|
+
route({
|
|
286
|
+
path: "/settings",
|
|
287
|
+
component: SettingsLayout, // Adds settings tabs
|
|
288
|
+
children: [
|
|
289
|
+
route({
|
|
290
|
+
path: "/",
|
|
291
|
+
component: GeneralSettings,
|
|
292
|
+
}),
|
|
293
|
+
route({
|
|
294
|
+
path: "/security",
|
|
295
|
+
component: SecuritySettings,
|
|
296
|
+
}),
|
|
297
|
+
route({
|
|
298
|
+
path: "/notifications",
|
|
299
|
+
component: NotificationSettings,
|
|
300
|
+
}),
|
|
301
|
+
],
|
|
302
|
+
}),
|
|
303
|
+
],
|
|
304
|
+
}),
|
|
305
|
+
],
|
|
306
|
+
}),
|
|
307
|
+
];`}</CodeBlock>
|
|
308
|
+
<p>
|
|
309
|
+
When you navigate to <code>/dashboard/settings/security</code>, the
|
|
310
|
+
rendering stack looks like:
|
|
311
|
+
</p>
|
|
312
|
+
<ol>
|
|
313
|
+
<li>
|
|
314
|
+
<code>RootLayout</code> renders the header and footer with an{" "}
|
|
315
|
+
<code>{"<Outlet>"}</code>
|
|
316
|
+
</li>
|
|
317
|
+
<li>
|
|
318
|
+
<code>DashboardLayout</code> renders the sidebar with an{" "}
|
|
319
|
+
<code>{"<Outlet>"}</code>
|
|
320
|
+
</li>
|
|
321
|
+
<li>
|
|
322
|
+
<code>SettingsLayout</code> renders the settings tabs with an{" "}
|
|
323
|
+
<code>{"<Outlet>"}</code>
|
|
324
|
+
</li>
|
|
325
|
+
<li>
|
|
326
|
+
<code>SecuritySettings</code> renders the actual page content
|
|
327
|
+
</li>
|
|
328
|
+
</ol>
|
|
329
|
+
</section>
|
|
330
|
+
|
|
331
|
+
<section>
|
|
332
|
+
<h3>Sharing Data with Loaders</h3>
|
|
333
|
+
<p>
|
|
334
|
+
Parent routes can load data that child routes need. This is
|
|
335
|
+
particularly useful for loading user information, permissions, or
|
|
336
|
+
other shared data once at the parent level.
|
|
337
|
+
</p>
|
|
338
|
+
<CodeBlock language="tsx">{`import { use, Suspense } from "react";
|
|
339
|
+
import { route, Outlet, useRouteData } from "@funstack/router";
|
|
340
|
+
|
|
341
|
+
// Define the parent route with a loader
|
|
342
|
+
const teamRoute = route({
|
|
343
|
+
id: "team",
|
|
344
|
+
path: "/teams/:teamId",
|
|
345
|
+
component: TeamLayout,
|
|
346
|
+
loader: async ({ params }) => {
|
|
347
|
+
const response = await fetch(\`/api/teams/\${params.teamId}\`);
|
|
348
|
+
return response.json();
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// Parent layout loads team data once
|
|
353
|
+
function TeamLayoutContent({
|
|
354
|
+
data,
|
|
355
|
+
}: {
|
|
356
|
+
data: Promise<{ name: string; members: string[] }>;
|
|
357
|
+
}) {
|
|
358
|
+
const team = use(data);
|
|
359
|
+
return (
|
|
360
|
+
<div>
|
|
361
|
+
<h1>{team.name}</h1>
|
|
362
|
+
<nav>
|
|
363
|
+
<a href="members">Members ({team.members.length})</a>
|
|
364
|
+
<a href="settings">Settings</a>
|
|
365
|
+
</nav>
|
|
366
|
+
<Outlet />
|
|
367
|
+
</div>
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function TeamLayout(props: {
|
|
372
|
+
data: Promise<{ name: string; members: string[] }>;
|
|
373
|
+
}) {
|
|
374
|
+
return (
|
|
375
|
+
<Suspense fallback={<div>Loading team...</div>}>
|
|
376
|
+
<TeamLayoutContent {...props} />
|
|
377
|
+
</Suspense>
|
|
378
|
+
);
|
|
379
|
+
}`}</CodeBlock>
|
|
380
|
+
<p>
|
|
381
|
+
Child routes can access the parent's loaded data using the{" "}
|
|
382
|
+
<code>useRouteData</code> hook with the parent's route ID:
|
|
383
|
+
</p>
|
|
384
|
+
<CodeBlock language="tsx">{`function TeamMembers() {
|
|
385
|
+
// Access parent route's data by route ID
|
|
386
|
+
const teamData = useRouteData(teamRoute);
|
|
387
|
+
const team = use(teamData);
|
|
388
|
+
|
|
389
|
+
return (
|
|
390
|
+
<ul>
|
|
391
|
+
{team.members.map((member) => (
|
|
392
|
+
<li key={member}>{member}</li>
|
|
393
|
+
))}
|
|
394
|
+
</ul>
|
|
395
|
+
);
|
|
396
|
+
}`}</CodeBlock>
|
|
397
|
+
</section>
|
|
398
|
+
|
|
399
|
+
<section>
|
|
400
|
+
<h3>Layout Routes Without Paths</h3>
|
|
401
|
+
<p>
|
|
402
|
+
Sometimes you want to wrap a group of routes in a layout without
|
|
403
|
+
adding a path segment. These are called <b>pathless routes</b>
|
|
404
|
+
—routes that provide UI structure without affecting the URL.
|
|
405
|
+
</p>
|
|
406
|
+
<CodeBlock language="tsx">{`const routes = [
|
|
407
|
+
route({
|
|
408
|
+
path: "/",
|
|
409
|
+
component: RootLayout,
|
|
410
|
+
children: [
|
|
411
|
+
route({ path: "/", component: HomePage }),
|
|
412
|
+
route({ path: "/about", component: AboutPage }),
|
|
413
|
+
|
|
414
|
+
// Pathless route - doesn't add to the URL
|
|
415
|
+
route({
|
|
416
|
+
component: AuthenticatedLayout, // No path property
|
|
417
|
+
children: [
|
|
418
|
+
route({ path: "/dashboard", component: Dashboard }),
|
|
419
|
+
route({ path: "/profile", component: Profile }),
|
|
420
|
+
route({ path: "/settings", component: Settings }),
|
|
421
|
+
],
|
|
422
|
+
}),
|
|
423
|
+
],
|
|
424
|
+
}),
|
|
425
|
+
];`}</CodeBlock>
|
|
426
|
+
<p>
|
|
427
|
+
The <code>AuthenticatedLayout</code> component wraps{" "}
|
|
428
|
+
<code>/dashboard</code>, <code>/profile</code>, and{" "}
|
|
429
|
+
<code>/settings</code> without adding anything to their URLs. This is
|
|
430
|
+
perfect for:
|
|
431
|
+
</p>
|
|
432
|
+
<ul>
|
|
433
|
+
<li>Authentication wrappers that check if users are logged in</li>
|
|
434
|
+
<li>Feature flag wrappers that conditionally show features</li>
|
|
435
|
+
<li>Context providers that supply data to a group of routes</li>
|
|
436
|
+
<li>Error boundaries for a subset of your application</li>
|
|
437
|
+
</ul>
|
|
438
|
+
<p>
|
|
439
|
+
Pathless routes also play a key role in server-side rendering. During
|
|
440
|
+
SSR, only pathless routes render (since no URL is available on the
|
|
441
|
+
server), making them ideal for defining the app shell. See the{" "}
|
|
442
|
+
<a href="/funstack-router/learn/server-side-rendering">
|
|
443
|
+
Server-Side Rendering
|
|
444
|
+
</a>{" "}
|
|
445
|
+
page for details.
|
|
446
|
+
</p>
|
|
447
|
+
</section>
|
|
448
|
+
|
|
449
|
+
<section>
|
|
450
|
+
<h3>Building a Complete Example</h3>
|
|
451
|
+
<p>
|
|
452
|
+
Let's put it all together with a realistic example—a project
|
|
453
|
+
management application with nested layouts.
|
|
454
|
+
</p>
|
|
455
|
+
<CodeBlock language="tsx">{`import { Router, route, Outlet } from "@funstack/router";
|
|
456
|
+
import { Suspense } from "react";
|
|
457
|
+
|
|
458
|
+
// Root layout with app-wide header
|
|
459
|
+
function AppLayout() {
|
|
460
|
+
return (
|
|
461
|
+
<div className="app">
|
|
462
|
+
<header>
|
|
463
|
+
<h1>Project Manager</h1>
|
|
464
|
+
<nav>
|
|
465
|
+
<a href="/">Home</a>
|
|
466
|
+
<a href="/projects">Projects</a>
|
|
467
|
+
</nav>
|
|
468
|
+
</header>
|
|
469
|
+
<Outlet />
|
|
470
|
+
<footer>© 2024 Project Manager</footer>
|
|
471
|
+
</div>
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Projects section layout with project list sidebar
|
|
476
|
+
function ProjectsLayout() {
|
|
477
|
+
return (
|
|
478
|
+
<div className="projects-layout">
|
|
479
|
+
<aside className="project-list">
|
|
480
|
+
<h2>Your Projects</h2>
|
|
481
|
+
{/* Project list would be loaded here */}
|
|
482
|
+
</aside>
|
|
483
|
+
<main>
|
|
484
|
+
<Outlet />
|
|
485
|
+
</main>
|
|
486
|
+
</div>
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Individual project layout with project-specific navigation
|
|
491
|
+
function ProjectLayout({ params }: { params: { projectId: string } }) {
|
|
492
|
+
return (
|
|
493
|
+
<div className="project">
|
|
494
|
+
<nav className="project-nav">
|
|
495
|
+
<a href={\`/projects/\${params.projectId}\`}>Overview</a>
|
|
496
|
+
<a href={\`/projects/\${params.projectId}/tasks\`}>Tasks</a>
|
|
497
|
+
<a href={\`/projects/\${params.projectId}/team\`}>Team</a>
|
|
498
|
+
</nav>
|
|
499
|
+
<Outlet />
|
|
500
|
+
</div>
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Route definitions
|
|
505
|
+
const routes = [
|
|
506
|
+
route({
|
|
507
|
+
path: "/",
|
|
508
|
+
component: AppLayout,
|
|
509
|
+
children: [
|
|
510
|
+
route({
|
|
511
|
+
path: "/",
|
|
512
|
+
component: HomePage,
|
|
513
|
+
}),
|
|
514
|
+
route({
|
|
515
|
+
path: "/projects",
|
|
516
|
+
component: ProjectsLayout,
|
|
517
|
+
children: [
|
|
518
|
+
route({
|
|
519
|
+
path: "/",
|
|
520
|
+
component: ProjectListPage,
|
|
521
|
+
}),
|
|
522
|
+
route({
|
|
523
|
+
path: "/:projectId",
|
|
524
|
+
component: ProjectLayout,
|
|
525
|
+
children: [
|
|
526
|
+
route({
|
|
527
|
+
path: "/",
|
|
528
|
+
component: ProjectOverview,
|
|
529
|
+
}),
|
|
530
|
+
route({
|
|
531
|
+
path: "/tasks",
|
|
532
|
+
component: ProjectTasks,
|
|
533
|
+
}),
|
|
534
|
+
route({
|
|
535
|
+
path: "/tasks/:taskId",
|
|
536
|
+
component: TaskDetail,
|
|
537
|
+
}),
|
|
538
|
+
route({
|
|
539
|
+
path: "/team",
|
|
540
|
+
component: ProjectTeam,
|
|
541
|
+
}),
|
|
542
|
+
],
|
|
543
|
+
}),
|
|
544
|
+
],
|
|
545
|
+
}),
|
|
546
|
+
],
|
|
547
|
+
}),
|
|
548
|
+
];
|
|
549
|
+
|
|
550
|
+
function App() {
|
|
551
|
+
return <Router routes={routes} />;
|
|
552
|
+
}`}</CodeBlock>
|
|
553
|
+
<p>With this structure:</p>
|
|
554
|
+
<ul>
|
|
555
|
+
<li>
|
|
556
|
+
<code>/</code> shows <code>AppLayout</code> →{" "}
|
|
557
|
+
<code>HomePage</code>
|
|
558
|
+
</li>
|
|
559
|
+
<li>
|
|
560
|
+
<code>/projects</code> shows <code>AppLayout</code> →{" "}
|
|
561
|
+
<code>ProjectsLayout</code> → <code>ProjectListPage</code>
|
|
562
|
+
</li>
|
|
563
|
+
<li>
|
|
564
|
+
<code>/projects/123</code> shows <code>AppLayout</code> →{" "}
|
|
565
|
+
<code>ProjectsLayout</code> → <code>ProjectLayout</code> →{" "}
|
|
566
|
+
<code>ProjectOverview</code>
|
|
567
|
+
</li>
|
|
568
|
+
<li>
|
|
569
|
+
<code>/projects/123/tasks/456</code> shows <code>AppLayout</code>{" "}
|
|
570
|
+
→ <code>ProjectsLayout</code> → <code>ProjectLayout</code>{" "}
|
|
571
|
+
→ <code>TaskDetail</code> (with both{" "}
|
|
572
|
+
<code>projectId: "123"</code> and <code>taskId: "456"</code>)
|
|
573
|
+
</li>
|
|
574
|
+
</ul>
|
|
575
|
+
</section>
|
|
576
|
+
|
|
577
|
+
<section>
|
|
578
|
+
<h3>Key Takeaways</h3>
|
|
579
|
+
<ul>
|
|
580
|
+
<li>
|
|
581
|
+
Use <code>{"<Outlet>"}</code> to mark where child routes should
|
|
582
|
+
render
|
|
583
|
+
</li>
|
|
584
|
+
<li>Child route paths are relative to their parent route's path</li>
|
|
585
|
+
<li>
|
|
586
|
+
Parent routes use prefix matching; leaf routes use exact matching
|
|
587
|
+
</li>
|
|
588
|
+
<li>Use pathless routes for layouts that don't affect the URL</li>
|
|
589
|
+
<li>
|
|
590
|
+
Parent route loaders run before children, making them ideal for
|
|
591
|
+
shared data
|
|
592
|
+
</li>
|
|
593
|
+
<li>
|
|
594
|
+
Deep nesting is supported—compose as many layout levels as you
|
|
595
|
+
need
|
|
596
|
+
</li>
|
|
597
|
+
</ul>
|
|
598
|
+
</section>
|
|
599
|
+
</div>
|
|
600
|
+
);
|
|
601
|
+
}
|