@bromscandium/router 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,411 +1,411 @@
1
- /**
2
- * Router components: Link, RouterView, Redirect, and Navigate.
3
- * @module
4
- */
5
-
6
- import {ref} from '@bromscandium/core';
7
- import {jsx, VNode} from '@bromscandium/runtime';
8
- import {useRoute, useRouter} from './hooks.js';
9
- import {NavigationTarget} from './router.js';
10
-
11
- /**
12
- * Props for the Link component.
13
- */
14
- interface LinkProps {
15
- /** The target path or navigation object */
16
- to: string | NavigationTarget;
17
- /** Child elements to render inside the link */
18
- children?: any;
19
- /** CSS class name */
20
- className?: string;
21
- /** Class name to add when route is active (includes nested) */
22
- activeClassName?: string;
23
- /** Class name to add when route exactly matches */
24
- exactActiveClassName?: string;
25
- /** If true, replace current history entry instead of pushing */
26
- replace?: boolean;
27
- /** HTML target attribute (e.g., "_blank") */
28
- target?: string;
29
- /** HTML rel attribute for external links */
30
- rel?: string;
31
- /** Additional click handler */
32
- onClick?: (e: MouseEvent) => void;
33
- /** Additional attributes passed to the anchor element */
34
- [key: string]: any;
35
- }
36
-
37
- /**
38
- * A navigation link component that integrates with the router.
39
- * Handles click events to perform client-side navigation.
40
- *
41
- * @param props - Link component props
42
- * @returns An anchor element VNode
43
- *
44
- * @example
45
- * ```tsx
46
- * <Link to="/about">About</Link>
47
- *
48
- * <Link to="/dashboard" activeClassName="active">Dashboard</Link>
49
- *
50
- * <Link to={{ path: '/user', query: { id: '123' } }}>User</Link>
51
- * ```
52
- */
53
- export function Link(props: LinkProps): VNode {
54
- const router = useRouter();
55
- const route = useRoute();
56
-
57
- const {
58
- to,
59
- children,
60
- className = '',
61
- activeClassName = '',
62
- exactActiveClassName = '',
63
- replace = false,
64
- target,
65
- rel,
66
- onClick,
67
- ...rest
68
- } = props;
69
-
70
- const href = typeof to === 'string' ? to : (to.path || '/');
71
-
72
- const currentPath = route.value.path;
73
- const base = router.base || '';
74
- const comparePath = base ? href.replace(new RegExp(`^${base}`), '') || '/' : href;
75
-
76
- const isExactActive = currentPath === comparePath;
77
- const isActive = isExactActive || currentPath.startsWith(comparePath + '/');
78
-
79
- let finalClassName = className;
80
- if (isActive && activeClassName) {
81
- finalClassName = `${finalClassName} ${activeClassName}`.trim();
82
- }
83
- if (isExactActive && exactActiveClassName) {
84
- finalClassName = `${finalClassName} ${exactActiveClassName}`.trim();
85
- }
86
-
87
- function handleClick(e: MouseEvent) {
88
- onClick?.(e);
89
-
90
- if (e.defaultPrevented) return;
91
-
92
- if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return;
93
-
94
- if (target) return;
95
-
96
- e.preventDefault();
97
-
98
- if (replace) {
99
- router.replace(to);
100
- } else {
101
- router.push(to);
102
- }
103
- }
104
-
105
- return jsx('a', {
106
- href,
107
- className: finalClassName || undefined,
108
- target,
109
- rel,
110
- onClick: handleClick,
111
- children,
112
- ...rest,
113
- });
114
- }
115
-
116
- /**
117
- * Props for the RouterView component.
118
- */
119
- interface RouterViewProps {
120
- /** The nesting depth for nested RouterViews (usually auto-detected) */
121
- depth?: number;
122
- }
123
-
124
- let routerViewDepth = 0;
125
-
126
- const componentCache = new Map<string, any>();
127
-
128
- const layoutCache = new Map<string, any>();
129
-
130
- const pendingLoads = new Map<string, Promise<any>>();
131
-
132
- const renderVersion = ref(0);
133
-
134
- /**
135
- * Renders the component for the current matched route.
136
- * Handles lazy loading and layout wrapping automatically.
137
- *
138
- * @param props - RouterView component props
139
- * @returns The rendered route component or null
140
- *
141
- * @example
142
- * ```tsx
143
- * function App() {
144
- * return (
145
- * <div>
146
- * <nav>...</nav>
147
- * <RouterView />
148
- * </div>
149
- * );
150
- * }
151
- * ```
152
- */
153
- export function RouterView(props: RouterViewProps): VNode | null {
154
- const { depth: explicitDepth } = props;
155
- const route = useRoute();
156
-
157
- void renderVersion.value;
158
-
159
- const currentDepth = explicitDepth ?? routerViewDepth;
160
- const matched = route.value.matched[currentDepth];
161
-
162
- if (!matched) {
163
- return null;
164
- }
165
-
166
- const routePath = matched.path;
167
- const routeConfig = matched.route;
168
-
169
- if (routeConfig.layout) {
170
- const layoutKey = `layout:${routePath}`;
171
- return loadAndRenderLayoutWithPage(
172
- routeConfig.layout,
173
- layoutKey,
174
- routeConfig.component,
175
- routePath,
176
- route.value.fullPath,
177
- currentDepth
178
- );
179
- }
180
-
181
- return loadAndRenderComponent(
182
- routeConfig.component,
183
- routePath,
184
- route.value.fullPath,
185
- currentDepth
186
- );
187
- }
188
-
189
- function loadAndRenderComponent(
190
- componentModule: () => Promise<{ default: any }> | { default: any },
191
- routePath: string,
192
- fullPath: string,
193
- currentDepth: number
194
- ): VNode | null {
195
- if (componentCache.has(routePath)) {
196
- const Component = componentCache.get(routePath);
197
- routerViewDepth = currentDepth + 1;
198
- try {
199
- return jsx(Component, {
200
- key: fullPath,
201
- });
202
- } finally {
203
- routerViewDepth = currentDepth;
204
- }
205
- }
206
-
207
- if (typeof componentModule === 'function') {
208
- if (!pendingLoads.has(routePath)) {
209
- const loadPromise = componentModule();
210
-
211
- if (loadPromise && typeof (loadPromise as any).then === 'function') {
212
- pendingLoads.set(routePath, loadPromise as Promise<any>);
213
-
214
- (loadPromise as Promise<any>).then((result) => {
215
- const Component = result.default || result;
216
- componentCache.set(routePath, Component);
217
- pendingLoads.delete(routePath);
218
- renderVersion.value++;
219
- }).catch((error) => {
220
- console.error('Failed to load route component:', error);
221
- pendingLoads.delete(routePath);
222
- });
223
-
224
- return jsx('div', {
225
- className: 'router-loading',
226
- children: '',
227
- });
228
- } else {
229
- const Component = (loadPromise as any).default || loadPromise;
230
- componentCache.set(routePath, Component);
231
-
232
- routerViewDepth = currentDepth + 1;
233
- try {
234
- return jsx(Component, {
235
- key: fullPath,
236
- });
237
- } finally {
238
- routerViewDepth = currentDepth;
239
- }
240
- }
241
- } else {
242
- return jsx('div', {
243
- className: 'router-loading',
244
- children: '',
245
- });
246
- }
247
- } else {
248
- const Component = (componentModule as any).default || componentModule;
249
- componentCache.set(routePath, Component);
250
-
251
- routerViewDepth = currentDepth + 1;
252
- try {
253
- return jsx(Component, {
254
- key: fullPath,
255
- });
256
- } finally {
257
- routerViewDepth = currentDepth;
258
- }
259
- }
260
-
261
- return null;
262
- }
263
-
264
- function loadAndRenderLayoutWithPage(
265
- layoutModule: () => Promise<{ default: any }> | { default: any },
266
- layoutKey: string,
267
- pageModule: () => Promise<{ default: any }> | { default: any },
268
- routePath: string,
269
- fullPath: string,
270
- currentDepth: number
271
- ): VNode | null {
272
- const layoutLoaded = layoutCache.has(layoutKey);
273
- const pageLoaded = componentCache.has(routePath);
274
-
275
- if (layoutLoaded && pageLoaded) {
276
- const Layout = layoutCache.get(layoutKey);
277
- const PageComponent = componentCache.get(routePath);
278
-
279
- routerViewDepth = currentDepth + 1;
280
- try {
281
- const pageElement = jsx(PageComponent, { key: fullPath });
282
- return jsx(Layout, { children: pageElement });
283
- } finally {
284
- routerViewDepth = currentDepth;
285
- }
286
- }
287
-
288
- if (!layoutLoaded && typeof layoutModule === 'function' && !pendingLoads.has(layoutKey)) {
289
- const loadPromise = layoutModule();
290
-
291
- if (loadPromise && typeof (loadPromise as any).then === 'function') {
292
- pendingLoads.set(layoutKey, loadPromise as Promise<any>);
293
-
294
- (loadPromise as Promise<any>).then((result) => {
295
- const Layout = result.default || result;
296
- layoutCache.set(layoutKey, Layout);
297
- pendingLoads.delete(layoutKey);
298
- renderVersion.value++;
299
- }).catch((error) => {
300
- console.error('Failed to load layout:', error);
301
- pendingLoads.delete(layoutKey);
302
- });
303
- } else {
304
- const Layout = (loadPromise as any).default || loadPromise;
305
- layoutCache.set(layoutKey, Layout);
306
- }
307
- }
308
-
309
- if (!pageLoaded && typeof pageModule === 'function' && !pendingLoads.has(routePath)) {
310
- const loadPromise = pageModule();
311
-
312
- if (loadPromise && typeof (loadPromise as any).then === 'function') {
313
- pendingLoads.set(routePath, loadPromise as Promise<any>);
314
-
315
- (loadPromise as Promise<any>).then((result) => {
316
- const PageComponent = result.default || result;
317
- componentCache.set(routePath, PageComponent);
318
- pendingLoads.delete(routePath);
319
- renderVersion.value++;
320
- }).catch((error) => {
321
- console.error('Failed to load page component:', error);
322
- pendingLoads.delete(routePath);
323
- });
324
- } else {
325
- const PageComponent = (loadPromise as any).default || loadPromise;
326
- componentCache.set(routePath, PageComponent);
327
- }
328
- }
329
-
330
- return jsx('div', {
331
- className: 'router-loading',
332
- children: '',
333
- });
334
- }
335
-
336
- /**
337
- * Props for the Redirect component.
338
- */
339
- interface RedirectProps {
340
- /** The target path or navigation object */
341
- to: string | NavigationTarget;
342
- /** If true, replace current history entry (default: true) */
343
- replace?: boolean;
344
- }
345
-
346
- /**
347
- * A component that performs a redirect when rendered.
348
- * Useful for redirecting from one route to another.
349
- *
350
- * @param props - Redirect component props
351
- * @returns null (performs navigation as side effect)
352
- *
353
- * @example
354
- * ```tsx
355
- * // In a route component
356
- * if (!isAuthenticated) {
357
- * return <Redirect to="/login" />;
358
- * }
359
- * ```
360
- */
361
- export function Redirect(props: RedirectProps): null {
362
- const router = useRouter();
363
- const { to, replace = true } = props;
364
-
365
- if (replace) {
366
- router.replace(to);
367
- } else {
368
- router.push(to);
369
- }
370
-
371
- return null;
372
- }
373
-
374
- /**
375
- * Props for the Navigate component.
376
- */
377
- interface NavigateProps {
378
- /** The target path or navigation object */
379
- to: string | NavigationTarget;
380
- /** If true, replace current history entry instead of pushing */
381
- replace?: boolean;
382
- }
383
-
384
- /**
385
- * A component that performs navigation when rendered.
386
- * Alternative to using the router imperatively.
387
- *
388
- * @param props - Navigate component props
389
- * @returns null (performs navigation as side effect)
390
- *
391
- * @example
392
- * ```tsx
393
- * function AfterSubmit({ success }) {
394
- * if (success) {
395
- * return <Navigate to="/success" />;
396
- * }
397
- * return <div>Form</div>;
398
- * }
399
- * ```
400
- */
401
- export function Navigate(props: NavigateProps): null {
402
- const router = useRouter();
403
-
404
- if (props.replace) {
405
- router.replace(props.to);
406
- } else {
407
- router.push(props.to);
408
- }
409
-
410
- return null;
411
- }
1
+ /**
2
+ * Router components: Link, RouterView, Redirect, and Navigate.
3
+ * @module
4
+ */
5
+
6
+ import {ref} from '@bromscandium/core';
7
+ import {jsx, VNode} from '@bromscandium/runtime';
8
+ import {useRoute, useRouter} from './hooks.js';
9
+ import {NavigationTarget} from './router.js';
10
+
11
+ /**
12
+ * Props for the Link component.
13
+ */
14
+ interface LinkProps {
15
+ /** The target path or navigation object */
16
+ to: string | NavigationTarget;
17
+ /** Child elements to render inside the link */
18
+ children?: any;
19
+ /** CSS class name */
20
+ className?: string;
21
+ /** Class name to add when route is active (includes nested) */
22
+ activeClassName?: string;
23
+ /** Class name to add when route exactly matches */
24
+ exactActiveClassName?: string;
25
+ /** If true, replace current history entry instead of pushing */
26
+ replace?: boolean;
27
+ /** HTML target attribute (e.g., "_blank") */
28
+ target?: string;
29
+ /** HTML rel attribute for external links */
30
+ rel?: string;
31
+ /** Additional click handler */
32
+ onClick?: (e: MouseEvent) => void;
33
+ /** Additional attributes passed to the anchor element */
34
+ [key: string]: any;
35
+ }
36
+
37
+ /**
38
+ * A navigation link component that integrates with the router.
39
+ * Handles click events to perform client-side navigation.
40
+ *
41
+ * @param props - Link component props
42
+ * @returns An anchor element VNode
43
+ *
44
+ * @example
45
+ * ```tsx
46
+ * <Link to="/about">About</Link>
47
+ *
48
+ * <Link to="/dashboard" activeClassName="active">Dashboard</Link>
49
+ *
50
+ * <Link to={{ path: '/user', query: { id: '123' } }}>User</Link>
51
+ * ```
52
+ */
53
+ export function Link(props: LinkProps): VNode {
54
+ const router = useRouter();
55
+ const route = useRoute();
56
+
57
+ const {
58
+ to,
59
+ children,
60
+ className = '',
61
+ activeClassName = '',
62
+ exactActiveClassName = '',
63
+ replace = false,
64
+ target,
65
+ rel,
66
+ onClick,
67
+ ...rest
68
+ } = props;
69
+
70
+ const href = typeof to === 'string' ? to : (to.path || '/');
71
+
72
+ const currentPath = route.value.path;
73
+ const base = router.base || '';
74
+ const comparePath = base ? href.replace(new RegExp(`^${base}`), '') || '/' : href;
75
+
76
+ const isExactActive = currentPath === comparePath;
77
+ const isActive = isExactActive || currentPath.startsWith(comparePath + '/');
78
+
79
+ let finalClassName = className;
80
+ if (isActive && activeClassName) {
81
+ finalClassName = `${finalClassName} ${activeClassName}`.trim();
82
+ }
83
+ if (isExactActive && exactActiveClassName) {
84
+ finalClassName = `${finalClassName} ${exactActiveClassName}`.trim();
85
+ }
86
+
87
+ function handleClick(e: MouseEvent) {
88
+ onClick?.(e);
89
+
90
+ if (e.defaultPrevented) return;
91
+
92
+ if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return;
93
+
94
+ if (target) return;
95
+
96
+ e.preventDefault();
97
+
98
+ if (replace) {
99
+ router.replace(to);
100
+ } else {
101
+ router.push(to);
102
+ }
103
+ }
104
+
105
+ return jsx('a', {
106
+ href,
107
+ className: finalClassName || undefined,
108
+ target,
109
+ rel,
110
+ onClick: handleClick,
111
+ children,
112
+ ...rest,
113
+ });
114
+ }
115
+
116
+ /**
117
+ * Props for the RouterView component.
118
+ */
119
+ interface RouterViewProps {
120
+ /** The nesting depth for nested RouterViews (usually auto-detected) */
121
+ depth?: number;
122
+ }
123
+
124
+ let routerViewDepth = 0;
125
+
126
+ const componentCache = new Map<string, any>();
127
+
128
+ const layoutCache = new Map<string, any>();
129
+
130
+ const pendingLoads = new Map<string, Promise<any>>();
131
+
132
+ const renderVersion = ref(0);
133
+
134
+ /**
135
+ * Renders the component for the current matched route.
136
+ * Handles lazy loading and layout wrapping automatically.
137
+ *
138
+ * @param props - RouterView component props
139
+ * @returns The rendered route component or null
140
+ *
141
+ * @example
142
+ * ```tsx
143
+ * function App() {
144
+ * return (
145
+ * <div>
146
+ * <nav>...</nav>
147
+ * <RouterView />
148
+ * </div>
149
+ * );
150
+ * }
151
+ * ```
152
+ */
153
+ export function RouterView(props: RouterViewProps): VNode | null {
154
+ const { depth: explicitDepth } = props;
155
+ const route = useRoute();
156
+
157
+ void renderVersion.value;
158
+
159
+ const currentDepth = explicitDepth ?? routerViewDepth;
160
+ const matched = route.value.matched[currentDepth];
161
+
162
+ if (!matched) {
163
+ return null;
164
+ }
165
+
166
+ const routePath = matched.path;
167
+ const routeConfig = matched.route;
168
+
169
+ if (routeConfig.layout) {
170
+ const layoutKey = `layout:${routePath}`;
171
+ return loadAndRenderLayoutWithPage(
172
+ routeConfig.layout,
173
+ layoutKey,
174
+ routeConfig.component,
175
+ routePath,
176
+ route.value.fullPath,
177
+ currentDepth
178
+ );
179
+ }
180
+
181
+ return loadAndRenderComponent(
182
+ routeConfig.component,
183
+ routePath,
184
+ route.value.fullPath,
185
+ currentDepth
186
+ );
187
+ }
188
+
189
+ function loadAndRenderComponent(
190
+ componentModule: () => Promise<{ default: any }> | { default: any },
191
+ routePath: string,
192
+ fullPath: string,
193
+ currentDepth: number
194
+ ): VNode | null {
195
+ if (componentCache.has(routePath)) {
196
+ const Component = componentCache.get(routePath);
197
+ routerViewDepth = currentDepth + 1;
198
+ try {
199
+ return jsx(Component, {
200
+ key: fullPath,
201
+ });
202
+ } finally {
203
+ routerViewDepth = currentDepth;
204
+ }
205
+ }
206
+
207
+ if (typeof componentModule === 'function') {
208
+ if (!pendingLoads.has(routePath)) {
209
+ const loadPromise = componentModule();
210
+
211
+ if (loadPromise && typeof (loadPromise as any).then === 'function') {
212
+ pendingLoads.set(routePath, loadPromise as Promise<any>);
213
+
214
+ (loadPromise as Promise<any>).then((result) => {
215
+ const Component = result.default || result;
216
+ componentCache.set(routePath, Component);
217
+ pendingLoads.delete(routePath);
218
+ renderVersion.value++;
219
+ }).catch((error) => {
220
+ console.error('Failed to load route component:', error);
221
+ pendingLoads.delete(routePath);
222
+ });
223
+
224
+ return jsx('div', {
225
+ className: 'router-loading',
226
+ children: '',
227
+ });
228
+ } else {
229
+ const Component = (loadPromise as any).default || loadPromise;
230
+ componentCache.set(routePath, Component);
231
+
232
+ routerViewDepth = currentDepth + 1;
233
+ try {
234
+ return jsx(Component, {
235
+ key: fullPath,
236
+ });
237
+ } finally {
238
+ routerViewDepth = currentDepth;
239
+ }
240
+ }
241
+ } else {
242
+ return jsx('div', {
243
+ className: 'router-loading',
244
+ children: '',
245
+ });
246
+ }
247
+ } else {
248
+ const Component = (componentModule as any).default || componentModule;
249
+ componentCache.set(routePath, Component);
250
+
251
+ routerViewDepth = currentDepth + 1;
252
+ try {
253
+ return jsx(Component, {
254
+ key: fullPath,
255
+ });
256
+ } finally {
257
+ routerViewDepth = currentDepth;
258
+ }
259
+ }
260
+
261
+ return null;
262
+ }
263
+
264
+ function loadAndRenderLayoutWithPage(
265
+ layoutModule: () => Promise<{ default: any }> | { default: any },
266
+ layoutKey: string,
267
+ pageModule: () => Promise<{ default: any }> | { default: any },
268
+ routePath: string,
269
+ fullPath: string,
270
+ currentDepth: number
271
+ ): VNode | null {
272
+ const layoutLoaded = layoutCache.has(layoutKey);
273
+ const pageLoaded = componentCache.has(routePath);
274
+
275
+ if (layoutLoaded && pageLoaded) {
276
+ const Layout = layoutCache.get(layoutKey);
277
+ const PageComponent = componentCache.get(routePath);
278
+
279
+ routerViewDepth = currentDepth + 1;
280
+ try {
281
+ const pageElement = jsx(PageComponent, { key: fullPath });
282
+ return jsx(Layout, { children: pageElement });
283
+ } finally {
284
+ routerViewDepth = currentDepth;
285
+ }
286
+ }
287
+
288
+ if (!layoutLoaded && typeof layoutModule === 'function' && !pendingLoads.has(layoutKey)) {
289
+ const loadPromise = layoutModule();
290
+
291
+ if (loadPromise && typeof (loadPromise as any).then === 'function') {
292
+ pendingLoads.set(layoutKey, loadPromise as Promise<any>);
293
+
294
+ (loadPromise as Promise<any>).then((result) => {
295
+ const Layout = result.default || result;
296
+ layoutCache.set(layoutKey, Layout);
297
+ pendingLoads.delete(layoutKey);
298
+ renderVersion.value++;
299
+ }).catch((error) => {
300
+ console.error('Failed to load layout:', error);
301
+ pendingLoads.delete(layoutKey);
302
+ });
303
+ } else {
304
+ const Layout = (loadPromise as any).default || loadPromise;
305
+ layoutCache.set(layoutKey, Layout);
306
+ }
307
+ }
308
+
309
+ if (!pageLoaded && typeof pageModule === 'function' && !pendingLoads.has(routePath)) {
310
+ const loadPromise = pageModule();
311
+
312
+ if (loadPromise && typeof (loadPromise as any).then === 'function') {
313
+ pendingLoads.set(routePath, loadPromise as Promise<any>);
314
+
315
+ (loadPromise as Promise<any>).then((result) => {
316
+ const PageComponent = result.default || result;
317
+ componentCache.set(routePath, PageComponent);
318
+ pendingLoads.delete(routePath);
319
+ renderVersion.value++;
320
+ }).catch((error) => {
321
+ console.error('Failed to load page component:', error);
322
+ pendingLoads.delete(routePath);
323
+ });
324
+ } else {
325
+ const PageComponent = (loadPromise as any).default || loadPromise;
326
+ componentCache.set(routePath, PageComponent);
327
+ }
328
+ }
329
+
330
+ return jsx('div', {
331
+ className: 'router-loading',
332
+ children: '',
333
+ });
334
+ }
335
+
336
+ /**
337
+ * Props for the Redirect component.
338
+ */
339
+ interface RedirectProps {
340
+ /** The target path or navigation object */
341
+ to: string | NavigationTarget;
342
+ /** If true, replace current history entry (default: true) */
343
+ replace?: boolean;
344
+ }
345
+
346
+ /**
347
+ * A component that performs a redirect when rendered.
348
+ * Useful for redirecting from one route to another.
349
+ *
350
+ * @param props - Redirect component props
351
+ * @returns null (performs navigation as side effect)
352
+ *
353
+ * @example
354
+ * ```tsx
355
+ * // In a route component
356
+ * if (!isAuthenticated) {
357
+ * return <Redirect to="/login" />;
358
+ * }
359
+ * ```
360
+ */
361
+ export function Redirect(props: RedirectProps): null {
362
+ const router = useRouter();
363
+ const { to, replace = true } = props;
364
+
365
+ if (replace) {
366
+ router.replace(to);
367
+ } else {
368
+ router.push(to);
369
+ }
370
+
371
+ return null;
372
+ }
373
+
374
+ /**
375
+ * Props for the Navigate component.
376
+ */
377
+ interface NavigateProps {
378
+ /** The target path or navigation object */
379
+ to: string | NavigationTarget;
380
+ /** If true, replace current history entry instead of pushing */
381
+ replace?: boolean;
382
+ }
383
+
384
+ /**
385
+ * A component that performs navigation when rendered.
386
+ * Alternative to using the router imperatively.
387
+ *
388
+ * @param props - Navigate component props
389
+ * @returns null (performs navigation as side effect)
390
+ *
391
+ * @example
392
+ * ```tsx
393
+ * function AfterSubmit({ success }) {
394
+ * if (success) {
395
+ * return <Navigate to="/success" />;
396
+ * }
397
+ * return <div>Form</div>;
398
+ * }
399
+ * ```
400
+ */
401
+ export function Navigate(props: NavigateProps): null {
402
+ const router = useRouter();
403
+
404
+ if (props.replace) {
405
+ router.replace(props.to);
406
+ } else {
407
+ router.push(props.to);
408
+ }
409
+
410
+ return null;
411
+ }