@funstack/router 0.0.7 → 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.
@@ -55,6 +55,39 @@ export function ApiComponentsPage() {
55
55
  the router will intercept the navigation.
56
56
  </td>
57
57
  </tr>
58
+ <tr>
59
+ <td>
60
+ <code>fallback</code>
61
+ </td>
62
+ <td>
63
+ <code>{'"none" | "static"'}</code>
64
+ </td>
65
+ <td>
66
+ Fallback mode when Navigation API is unavailable.{" "}
67
+ <code>"none"</code> (default) renders nothing;{" "}
68
+ <code>"static"</code> renders matched routes using{" "}
69
+ <code>window.location</code> without navigation interception
70
+ (MPA behavior).
71
+ </td>
72
+ </tr>
73
+ <tr>
74
+ <td>
75
+ <code>ssr</code>
76
+ </td>
77
+ <td>
78
+ <code>SSRConfig</code>
79
+ </td>
80
+ <td>
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.
89
+ </td>
90
+ </tr>
58
91
  </tbody>
59
92
  </table>
60
93
  </article>
@@ -163,9 +163,14 @@ function MyComponent() {
163
163
  <ul>
164
164
  <li>
165
165
  This hook is powered by React's <code>useTransition</code>. The
166
- router wraps navigation state updates in{" "}
167
- <code>startTransition</code>, so React defers rendering suspended
168
- routes and keeps the current UI visible.
166
+ router wraps navigations in <code>startTransition</code>, so React
167
+ defers rendering suspended routes and keeps the current UI visible.
168
+ </li>
169
+ <li>
170
+ Sync state updates via <code>setStateSync</code> and{" "}
171
+ <code>resetStateSync</code> bypass transitions entirely, so{" "}
172
+ <code>isPending</code> will <strong>not</strong> become{" "}
173
+ <code>true</code> for those updates.
169
174
  </li>
170
175
  <li>
171
176
  The same <code>isPending</code> value is also available as a prop on
@@ -37,7 +37,10 @@ type Props = {
37
37
  state: { scrollPosition: number } |
38
38
  ((prev: { scrollPosition: number } | undefined) => { scrollPosition: number })
39
39
  ) => void;
40
- resetState: () => void;
40
+ // Async reset via replace navigation
41
+ resetState: () => Promise<void>;
42
+ // Sync reset via updateCurrentEntry
43
+ resetStateSync: () => void;
41
44
  info: unknown; // Ephemeral navigation info
42
45
  isPending: boolean; // Whether a navigation transition is pending
43
46
  };`}</CodeBlock>
@@ -48,11 +51,27 @@ type Props = {
48
51
  <li>
49
52
  <code>setState</code> - Async method that returns a Promise. Uses
50
53
  replace navigation internally, ensuring the state update goes
51
- through the full navigation cycle.
54
+ through the full navigation cycle. Because it performs a navigation,
55
+ it is wrapped in a React transition and may set{" "}
56
+ <code>isPending</code> to <code>true</code>.
52
57
  </li>
53
58
  <li>
54
59
  <code>setStateSync</code> - Synchronous method that updates state
55
- immediately using <code>navigation.updateCurrentEntry()</code>.
60
+ immediately using <code>navigation.updateCurrentEntry()</code>. This
61
+ is <strong>not</strong> a navigation, so it bypasses React
62
+ transitions and will never set <code>isPending</code> to{" "}
63
+ <code>true</code>.
64
+ </li>
65
+ <li>
66
+ <code>resetState</code> - Async method that clears navigation state
67
+ via replace navigation. Like <code>setState</code>, it goes through
68
+ a React transition and may set <code>isPending</code> to{" "}
69
+ <code>true</code>.
70
+ </li>
71
+ <li>
72
+ <code>resetStateSync</code> - Clears navigation state synchronously.
73
+ Like <code>setStateSync</code>, this bypasses React transitions and
74
+ will never set <code>isPending</code> to <code>true</code>.
56
75
  </li>
57
76
  </ul>
58
77
  </article>
@@ -80,7 +99,8 @@ type Props = {
80
99
  state: { selectedTab: string } | undefined;
81
100
  setState: (state: ...) => Promise<void>; // async
82
101
  setStateSync: (state: ...) => void; // sync
83
- resetState: () => void;
102
+ resetState: () => Promise<void>; // async
103
+ resetStateSync: () => void; // sync
84
104
  info: unknown; // Ephemeral navigation info
85
105
  isPending: boolean; // Whether a navigation transition is pending
86
106
  };`}</CodeBlock>
@@ -238,8 +258,9 @@ type SettingsPageProps = RouteComponentPropsOf<typeof settingsRoute>;
238
258
  <code>component: UserPage</code>): Router automatically injects
239
259
  props (<code>params</code>, <code>state</code>,{" "}
240
260
  <code>setState</code>, <code>setStateSync</code>,{" "}
241
- <code>resetState</code>, <code>info</code>, <code>isPending</code>,
242
- and <code>data</code> when a loader is defined).
261
+ <code>resetState</code>, <code>resetStateSync</code>,{" "}
262
+ <code>info</code>, <code>isPending</code>, and <code>data</code>{" "}
263
+ when a loader is defined).
243
264
  </li>
244
265
  <li>
245
266
  <strong>JSX element</strong> (e.g.,{" "}
@@ -268,15 +289,42 @@ routeState<{ tab: string }>()({
268
289
  });`}</CodeBlock>
269
290
  </article>
270
291
 
292
+ <article className="api-item">
293
+ <h3>
294
+ <code>ActionArgs</code>
295
+ </h3>
296
+ <p>
297
+ Arguments passed to route action functions. The <code>request</code>{" "}
298
+ carries the POST method and <code>FormData</code> body from the form
299
+ submission.
300
+ </p>
301
+ <CodeBlock language="typescript">{`interface ActionArgs<Params> {
302
+ params: Params;
303
+ request: Request; // method: "POST", body: FormData
304
+ signal: AbortSignal;
305
+ }`}</CodeBlock>
306
+ </article>
307
+
271
308
  <article className="api-item">
272
309
  <h3>
273
310
  <code>LoaderArgs</code>
274
311
  </h3>
275
- <CodeBlock language="typescript">{`interface LoaderArgs {
276
- params: Record<string, string>;
312
+ <p>
313
+ Arguments passed to route loader functions. The optional{" "}
314
+ <code>actionResult</code> parameter contains the return value of the
315
+ route's action when the loader runs after a form submission.
316
+ </p>
317
+ <CodeBlock language="typescript">{`interface LoaderArgs<Params, ActionResult = undefined> {
318
+ params: Params;
277
319
  request: Request;
278
320
  signal: AbortSignal;
321
+ actionResult: ActionResult | undefined;
279
322
  }`}</CodeBlock>
323
+ <p>
324
+ On normal navigations, <code>actionResult</code> is{" "}
325
+ <code>undefined</code>. After a form submission, it contains the
326
+ action's return value (awaited if the action is async).
327
+ </p>
280
328
  </article>
281
329
 
282
330
  <article className="api-item">
@@ -17,8 +17,9 @@ export function ApiUtilitiesPage() {
17
17
  always receives a <code>params</code> prop with types inferred from
18
18
  the path pattern. When a <code>loader</code> is defined, the component
19
19
  also receives a <code>data</code> prop. Components also receive{" "}
20
- <code>state</code>, <code>setState</code>, <code>setStateSync</code>,
21
- and <code>resetState</code> props for navigation state management.
20
+ <code>state</code>, <code>setState</code>, <code>setStateSync</code>,{" "}
21
+ <code>resetState</code>, and <code>resetStateSync</code> props for
22
+ navigation state management.
22
23
  </p>
23
24
  <CodeBlock language="tsx">{`import { route } from "@funstack/router";
24
25
 
@@ -84,6 +85,20 @@ const myRoute = route({
84
85
  (and <code>data</code> prop if loader is defined)
85
86
  </td>
86
87
  </tr>
88
+ <tr>
89
+ <td>
90
+ <code>action</code>
91
+ </td>
92
+ <td>
93
+ <code>(args: ActionArgs) =&gt; T</code>
94
+ </td>
95
+ <td>
96
+ Function to handle form submissions (POST navigations). Receives
97
+ a <code>Request</code> with <code>FormData</code> body. The
98
+ return value is passed to the loader as{" "}
99
+ <code>actionResult</code>.
100
+ </td>
101
+ </tr>
87
102
  <tr>
88
103
  <td>
89
104
  <code>loader</code>
@@ -204,11 +219,15 @@ const productRoute = routeState<{ filter: string }>()({
204
219
  <li>
205
220
  <code>setState</code> - Async method that uses replace navigation.
206
221
  Returns a Promise that resolves when the navigation completes.
222
+ Because it performs a navigation, the update is wrapped in a React
223
+ transition (may set <code>isPending</code> to <code>true</code>).
207
224
  </li>
208
225
  <li>
209
226
  <code>setStateSync</code> - Sync method that uses{" "}
210
227
  <code>navigation.updateCurrentEntry()</code>. Updates state
211
- immediately without waiting.
228
+ immediately without waiting. This is not a navigation, so it
229
+ bypasses React transitions entirely (<code>isPending</code> stays{" "}
230
+ <code>false</code>).
212
231
  </li>
213
232
  </ul>
214
233
  <p>Navigation state characteristics:</p>
@@ -276,8 +295,9 @@ const routes = [
276
295
  <code>routeState</code> - Route definition helper with typed state
277
296
  </li>
278
297
  <li>
279
- Types: <code>LoaderArgs</code>, <code>RouteDefinition</code>,{" "}
280
- <code>PathParams</code>, <code>RouteComponentProps</code>,{" "}
298
+ Types: <code>ActionArgs</code>, <code>LoaderArgs</code>,{" "}
299
+ <code>RouteDefinition</code>, <code>PathParams</code>,{" "}
300
+ <code>RouteComponentProps</code>,{" "}
281
301
  <code>RouteComponentPropsWithData</code>
282
302
  </li>
283
303
  </ul>
@@ -287,9 +307,7 @@ const routes = [
287
307
  </p>
288
308
  <p>
289
309
  See the{" "}
290
- <a href="/funstack-router/learn/react-server-components">
291
- React Server Components
292
- </a>{" "}
310
+ <a href="/learn/react-server-components">React Server Components</a>{" "}
293
311
  guide for a full walkthrough of using the server entry point.
294
312
  </p>
295
313
  </article>
@@ -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>&lt;Suspense&gt;</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>&lt;form&gt;</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
+ }