@esmx/router-react 3.0.0-rc.105

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/README.md ADDED
@@ -0,0 +1,515 @@
1
+ <div align="center">
2
+ <img src="https://esmx.dev/logo.svg?t=2025" width="120" alt="Esmx Logo" />
3
+ <h1>@esmx/router-react</h1>
4
+
5
+ <div>
6
+ <a href="https://www.npmjs.com/package/@esmx/router-react">
7
+ <img src="https://img.shields.io/npm/v/@esmx/router-react.svg" alt="npm version" />
8
+ </a>
9
+ <a href="https://github.com/esmnext/esmx/actions/workflows/build.yml">
10
+ <img src="https://github.com/esmnext/esmx/actions/workflows/build.yml/badge.svg" alt="Build" />
11
+ </a>
12
+ <a href="https://esmx.dev/coverage/">
13
+ <img src="https://img.shields.io/badge/coverage-live%20report-brightgreen" alt="Coverage Report" />
14
+ </a>
15
+ <a href="https://nodejs.org/">
16
+ <img src="https://img.shields.io/node/v/@esmx/router-react.svg" alt="node version" />
17
+ </a>
18
+ <a href="https://bundlephobia.com/package/@esmx/router-react">
19
+ <img src="https://img.shields.io/bundlephobia/minzip/@esmx/router-react" alt="size" />
20
+ </a>
21
+ </div>
22
+
23
+ <p>React integration for <a href="https://github.com/esmnext/esmx/tree/master/packages/router">@esmx/router</a> - A powerful router with React 18+ support using modern hooks and context patterns.</p>
24
+
25
+ <p>
26
+ English | <a href="https://github.com/esmnext/esmx/blob/master/packages/router-react/README.zh-CN.md">δΈ­ζ–‡</a>
27
+ </p>
28
+ </div>
29
+
30
+ ## πŸš€ Features
31
+
32
+ ✨ **React 18+ Support** - Built for React 18 and 19 with hooks and concurrent features
33
+ 🎯 **Hooks First** - Modern hook-based API with `useRouter`, `useRoute`, `useLink`
34
+ πŸ”— **Seamless Integration** - Works with @esmx/router core
35
+ πŸš€ **TypeScript Ready** - Full TypeScript support with excellent DX
36
+ ⚑ **High Performance** - Uses `useSyncExternalStore` for optimal re-renders
37
+ πŸ”„ **SSR Compatible** - Server-side rendering support out of the box
38
+ πŸ“¦ **Lightweight** - Minimal bundle size with zero dependencies (except peer deps)
39
+ πŸ”§ **Pure TypeScript** - No JSX required, uses React.createElement for maximum compatibility
40
+
41
+ ## πŸ“¦ Installation
42
+
43
+ ```bash
44
+ # npm
45
+ npm install @esmx/router @esmx/router-react
46
+
47
+ # pnpm
48
+ pnpm add @esmx/router @esmx/router-react
49
+
50
+ # yarn
51
+ yarn add @esmx/router @esmx/router-react
52
+ ```
53
+
54
+ ## πŸš€ Quick Start
55
+
56
+ ```tsx
57
+ import { Router, RouterMode } from '@esmx/router';
58
+ import { RouterProvider, RouterView, RouterLink } from '@esmx/router-react';
59
+
60
+ // Define your routes
61
+ const routes = [
62
+ { path: '/', component: () => import('./views/Home') },
63
+ { path: '/about', component: () => import('./views/About') },
64
+ { path: '/users/:id', component: () => import('./views/UserProfile') }
65
+ ];
66
+
67
+ // Create router instance
68
+ const router = new Router({
69
+ routes,
70
+ mode: RouterMode.history
71
+ });
72
+
73
+ // Wrap your app with RouterProvider
74
+ function App() {
75
+ return (
76
+ <RouterProvider router={router}>
77
+ <nav>
78
+ <RouterLink to="/">Home</RouterLink>
79
+ <RouterLink to="/about">About</RouterLink>
80
+ <RouterLink to="/users/123">User Profile</RouterLink>
81
+ </nav>
82
+
83
+ <RouterView />
84
+ </RouterProvider>
85
+ );
86
+ }
87
+
88
+ export default App;
89
+ ```
90
+
91
+ ## πŸ“š API Reference
92
+
93
+ ### Components
94
+
95
+ #### RouterProvider
96
+
97
+ Provides router context to the React component tree. Must wrap your application.
98
+
99
+ ```tsx
100
+ import { Router } from '@esmx/router';
101
+ import { RouterProvider } from '@esmx/router-react';
102
+
103
+ const router = new Router({ routes });
104
+
105
+ function App() {
106
+ return (
107
+ <RouterProvider router={router}>
108
+ {/* Your app components */}
109
+ </RouterProvider>
110
+ );
111
+ }
112
+ ```
113
+
114
+ **Props:**
115
+
116
+ | Prop | Type | Required | Description |
117
+ |------|------|----------|-------------|
118
+ | `router` | `Router` | Yes | Router instance from @esmx/router |
119
+ | `children` | `ReactNode` | Yes | Child components |
120
+
121
+ #### RouterView
122
+
123
+ Renders the matched route component. Supports nested routing with automatic depth tracking.
124
+
125
+ ```tsx
126
+ import { RouterView } from '@esmx/router-react';
127
+
128
+ function Layout() {
129
+ return (
130
+ <div>
131
+ <header>My App</header>
132
+ <RouterView />
133
+ <footer>Footer</footer>
134
+ </div>
135
+ );
136
+ }
137
+ ```
138
+
139
+ **Props:**
140
+
141
+ | Prop | Type | Default | Description |
142
+ |------|------|---------|-------------|
143
+ | `fallback` | `ReactNode \| ComponentType` | - | Fallback content when no route matches |
144
+
145
+ **Nested Routing Example:**
146
+
147
+ ```tsx
148
+ // Routes configuration
149
+ const routes = [
150
+ {
151
+ path: '/users',
152
+ component: UsersLayout,
153
+ children: [
154
+ { path: '', component: UsersList },
155
+ { path: ':id', component: UserProfile }
156
+ ]
157
+ }
158
+ ];
159
+
160
+ // UsersLayout.tsx - Contains nested RouterView
161
+ function UsersLayout() {
162
+ return (
163
+ <div>
164
+ <h1>Users Section</h1>
165
+ <RouterView /> {/* Renders UsersList or UserProfile */}
166
+ </div>
167
+ );
168
+ }
169
+ ```
170
+
171
+ #### RouterLink
172
+
173
+ A component for creating navigation links with active state management.
174
+
175
+ ```tsx
176
+ import { RouterLink } from '@esmx/router-react';
177
+
178
+ function Navigation() {
179
+ return (
180
+ <nav>
181
+ {/* Basic link */}
182
+ <RouterLink to="/home">Home</RouterLink>
183
+
184
+ {/* With object location */}
185
+ <RouterLink to={{ path: '/search', query: { q: 'react' } }}>
186
+ Search
187
+ </RouterLink>
188
+
189
+ {/* Replace navigation */}
190
+ <RouterLink to="/login" type="replace">Login</RouterLink>
191
+
192
+ {/* Open in new window */}
193
+ <RouterLink to="/docs" type="pushWindow">Docs β†—</RouterLink>
194
+
195
+ {/* Custom active class */}
196
+ <RouterLink
197
+ to="/dashboard"
198
+ activeClass="nav-active"
199
+ exact="exact"
200
+ >
201
+ Dashboard
202
+ </RouterLink>
203
+
204
+ {/* Custom tag */}
205
+ <RouterLink to="/submit" tag="button" className="btn">
206
+ Submit
207
+ </RouterLink>
208
+ </nav>
209
+ );
210
+ }
211
+ ```
212
+
213
+ **Props:**
214
+
215
+ | Prop | Type | Default | Description |
216
+ |------|------|---------|-------------|
217
+ | `to` | `string \| RouteLocationInput` | - | Target route location |
218
+ | `type` | `RouterLinkType` | `'push'` | Navigation type |
219
+ | `exact` | `RouteMatchType` | `'include'` | Active matching mode |
220
+ | `activeClass` | `string` | - | CSS class for active state |
221
+ | `event` | `string \| string[]` | `'click'` | Events triggering navigation |
222
+ | `tag` | `string` | `'a'` | HTML tag to render |
223
+ | `layerOptions` | `RouterLayerOptions` | - | Layer navigation options |
224
+ | `beforeNavigate` | `function` | - | Callback before navigation |
225
+ | `children` | `ReactNode` | - | Link content |
226
+ | `className` | `string` | - | Additional CSS classes |
227
+ | `style` | `CSSProperties` | - | Inline styles |
228
+
229
+ **Navigation Types:**
230
+
231
+ - `'push'` - Add entry to history stack (default)
232
+ - `'replace'` - Replace current history entry
233
+ - `'pushWindow'` - Open in new window/tab
234
+ - `'replaceWindow'` - Replace current window location
235
+ - `'pushLayer'` - Open in layer (modal/dialog routing)
236
+
237
+ ### Hooks
238
+
239
+ #### useRouter()
240
+
241
+ Get the router instance for programmatic navigation.
242
+
243
+ ```tsx
244
+ import { useRouter } from '@esmx/router-react';
245
+
246
+ function NavigationControls() {
247
+ const router = useRouter();
248
+
249
+ const goHome = () => router.push('/');
250
+ const goBack = () => router.back();
251
+ const goForward = () => router.forward();
252
+
253
+ const goToUser = (id: string) => {
254
+ router.push({
255
+ path: '/users/:id',
256
+ params: { id }
257
+ });
258
+ };
259
+
260
+ const replaceRoute = () => {
261
+ router.replace('/login');
262
+ };
263
+
264
+ return (
265
+ <div>
266
+ <button onClick={goHome}>Home</button>
267
+ <button onClick={goBack}>Back</button>
268
+ <button onClick={goForward}>Forward</button>
269
+ <button onClick={() => goToUser('123')}>User 123</button>
270
+ <button onClick={replaceRoute}>Login (Replace)</button>
271
+ </div>
272
+ );
273
+ }
274
+ ```
275
+
276
+ #### useRoute()
277
+
278
+ Get the current route information (reactive).
279
+
280
+ ```tsx
281
+ import { useRoute } from '@esmx/router-react';
282
+ import { useEffect } from 'react';
283
+
284
+ function CurrentRoute() {
285
+ const route = useRoute();
286
+
287
+ useEffect(() => {
288
+ console.log('Route changed:', route.path);
289
+ document.title = route.meta?.title || 'My App';
290
+ }, [route.path, route.meta?.title]);
291
+
292
+ return (
293
+ <div>
294
+ <p>Path: {route.path}</p>
295
+ <p>Params: {JSON.stringify(route.params)}</p>
296
+ <p>Query: {JSON.stringify(route.query)}</p>
297
+ <p>Hash: {route.hash}</p>
298
+ <p>Meta: {JSON.stringify(route.meta)}</p>
299
+ </div>
300
+ );
301
+ }
302
+ ```
303
+
304
+ #### useLink()
305
+
306
+ Create reactive link helpers for custom navigation components.
307
+
308
+ ```tsx
309
+ import { useLink } from '@esmx/router-react';
310
+
311
+ interface CustomNavButtonProps {
312
+ to: string;
313
+ children: React.ReactNode;
314
+ }
315
+
316
+ function CustomNavButton({ to, children }: CustomNavButtonProps) {
317
+ const link = useLink({ to, type: 'push', exact: 'include' });
318
+
319
+ return (
320
+ <button
321
+ onClick={(e) => link.navigate(e)}
322
+ className={`nav-button ${link.isActive ? 'active' : ''}`}
323
+ aria-current={link.isExactActive ? 'page' : undefined}
324
+ >
325
+ {children}
326
+ {link.isExternal && <span>β†—</span>}
327
+ </button>
328
+ );
329
+ }
330
+ ```
331
+
332
+ **Return Value (`RouterLinkResolved`):**
333
+
334
+ | Property | Type | Description |
335
+ |----------|------|-------------|
336
+ | `route` | `Route` | Resolved route object |
337
+ | `type` | `RouterLinkType` | Navigation type |
338
+ | `isActive` | `boolean` | Whether link matches current route |
339
+ | `isExactActive` | `boolean` | Whether link exactly matches current route |
340
+ | `isExternal` | `boolean` | Whether link points to external origin |
341
+ | `tag` | `string` | HTML tag to render |
342
+ | `attributes` | `RouterLinkAttributes` | HTML attributes (href, class, target, rel) |
343
+ | `navigate` | `(e: Event) => Promise<void>` | Navigation function |
344
+ | `createEventHandlers` | `function` | Generate event handlers |
345
+
346
+ #### useRouterViewDepth()
347
+
348
+ Get the current RouterView depth (for advanced nested routing scenarios).
349
+
350
+ ```tsx
351
+ import { useRouterViewDepth } from '@esmx/router-react';
352
+
353
+ function DebugView() {
354
+ const depth = useRouterViewDepth();
355
+
356
+ return <div>Current depth: {depth}</div>;
357
+ }
358
+ ```
359
+
360
+ ### Context
361
+
362
+ #### RouterContext
363
+
364
+ Direct access to the router context (for advanced use cases).
365
+
366
+ ```tsx
367
+ import { RouterContext } from '@esmx/router-react';
368
+ import { useContext } from 'react';
369
+
370
+ function CustomRouterConsumer() {
371
+ const context = useContext(RouterContext);
372
+
373
+ if (!context) {
374
+ return <div>Not inside RouterProvider</div>;
375
+ }
376
+
377
+ const { router, route } = context;
378
+ // Use router and route directly
379
+ }
380
+ ```
381
+
382
+ ## Advanced Usage
383
+
384
+ ### Route Guards
385
+
386
+ ```tsx
387
+ import { useRouter, useRoute } from '@esmx/router-react';
388
+ import { useEffect } from 'react';
389
+
390
+ function AuthGuard({ children }: { children: React.ReactNode }) {
391
+ const router = useRouter();
392
+ const route = useRoute();
393
+
394
+ useEffect(() => {
395
+ // Register beforeEach guard
396
+ const unregister = router.beforeEach((to, from) => {
397
+ if (to.meta?.requiresAuth && !isAuthenticated()) {
398
+ return '/login';
399
+ }
400
+ });
401
+
402
+ return () => unregister();
403
+ }, [router]);
404
+
405
+ return <>{children}</>;
406
+ }
407
+ ```
408
+
409
+ ### SSR Integration
410
+
411
+ ```tsx
412
+ import { Router, RouterMode } from '@esmx/router';
413
+ import { RouterProvider, RouterView } from '@esmx/router-react';
414
+ import { renderToString } from 'react-dom/server';
415
+
416
+ async function renderApp(url: string) {
417
+ const router = new Router({
418
+ routes,
419
+ mode: RouterMode.history,
420
+ url // Pass the request URL
421
+ });
422
+
423
+ // Wait for route resolution
424
+ await router.push(url);
425
+
426
+ const html = renderToString(
427
+ <RouterProvider router={router}>
428
+ <App />
429
+ </RouterProvider>
430
+ );
431
+
432
+ return html;
433
+ }
434
+ ```
435
+
436
+ ### Custom Link Component with Additional Features
437
+
438
+ ```tsx
439
+ import { useLink } from '@esmx/router-react';
440
+ import { forwardRef, type AnchorHTMLAttributes } from 'react';
441
+
442
+ interface NavLinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
443
+ to: string;
444
+ icon?: React.ReactNode;
445
+ badge?: number;
446
+ }
447
+
448
+ export const NavLink = forwardRef<HTMLAnchorElement, NavLinkProps>(
449
+ ({ to, icon, badge, children, className, ...props }, ref) => {
450
+ const link = useLink({ to, exact: 'include' });
451
+
452
+ return (
453
+ <a
454
+ ref={ref}
455
+ href={link.attributes.href}
456
+ onClick={(e) => {
457
+ e.preventDefault();
458
+ link.navigate(e);
459
+ }}
460
+ className={`nav-link ${link.isActive ? 'active' : ''} ${className || ''}`}
461
+ aria-current={link.isExactActive ? 'page' : undefined}
462
+ {...props}
463
+ >
464
+ {icon && <span className="nav-icon">{icon}</span>}
465
+ <span className="nav-label">{children}</span>
466
+ {badge !== undefined && badge > 0 && (
467
+ <span className="nav-badge">{badge}</span>
468
+ )}
469
+ </a>
470
+ );
471
+ }
472
+ );
473
+
474
+ NavLink.displayName = 'NavLink';
475
+ ```
476
+
477
+ ## TypeScript Support
478
+
479
+ This package provides full TypeScript support. All types are exported and properly documented.
480
+
481
+ ```tsx
482
+ import type {
483
+ RouterContextValue,
484
+ RouterProviderProps,
485
+ RouterViewProps,
486
+ RouterLinkComponentProps
487
+ } from '@esmx/router-react';
488
+
489
+ import type {
490
+ Route,
491
+ Router,
492
+ RouterLinkProps,
493
+ RouterLinkResolved,
494
+ RouteLocationInput
495
+ } from '@esmx/router';
496
+ ```
497
+
498
+ ## Browser Support
499
+
500
+ - **Modern browsers** that support ES modules (`import`/`export`) and React 18+
501
+ - Chrome 64+, Firefox 67+, Safari 11.1+, Edge 79+
502
+
503
+ ## Contributing
504
+
505
+ We welcome contributions! Please feel free to submit issues and pull requests.
506
+
507
+ ## πŸ“„ License
508
+
509
+ MIT Β© [Esmx Team](https://github.com/esmnext/esmx)
510
+
511
+ ## Related Packages
512
+
513
+ - [@esmx/router](https://github.com/esmnext/esmx/tree/master/packages/router) - Core router package
514
+ - [@esmx/router-vue](https://github.com/esmnext/esmx/tree/master/packages/router-vue) - Vue integration
515
+ - [@esmx/core](https://github.com/esmnext/esmx/tree/master/packages/core) - Esmx core framework
@@ -0,0 +1,85 @@
1
+ import type { Route, Router } from '@esmx/router';
2
+ import type { RouterContextValue } from './types';
3
+ /**
4
+ * React Context for router state.
5
+ * Contains the router instance and current route.
6
+ * Using null as default to detect missing provider.
7
+ */
8
+ export declare const RouterContext: import("react").Context<RouterContextValue | null>;
9
+ /**
10
+ * React Context for RouterView depth tracking.
11
+ * Used for nested routing to determine which matched route to render.
12
+ */
13
+ export declare const RouterViewDepthContext: import("react").Context<number>;
14
+ /**
15
+ * Get the router context value.
16
+ * @throws {Error} If used outside of RouterProvider
17
+ * @internal
18
+ */
19
+ export declare function useRouterContext(): RouterContextValue;
20
+ /**
21
+ * Get the router instance for navigation.
22
+ * Must be used within a RouterProvider.
23
+ *
24
+ * @returns Router instance with navigation methods
25
+ * @throws {Error} If used outside of RouterProvider
26
+ *
27
+ * @example
28
+ * ```tsx
29
+ * import { useRouter } from '@esmx/router-react';
30
+ *
31
+ * function NavigationButton() {
32
+ * const router = useRouter();
33
+ *
34
+ * const handleClick = () => {
35
+ * router.push('/dashboard');
36
+ * };
37
+ *
38
+ * return <button onClick={handleClick}>Go to Dashboard</button>;
39
+ * }
40
+ * ```
41
+ */
42
+ export declare function useRouter(): Router;
43
+ /**
44
+ * Get the current route information.
45
+ * Returns a reactive route object that updates when navigation occurs.
46
+ * Must be used within a RouterProvider.
47
+ *
48
+ * @returns Current route object with path, params, query, etc.
49
+ * @throws {Error} If used outside of RouterProvider
50
+ *
51
+ * @example
52
+ * ```tsx
53
+ * import { useRoute } from '@esmx/router-react';
54
+ *
55
+ * function CurrentPath() {
56
+ * const route = useRoute();
57
+ *
58
+ * return (
59
+ * <div>
60
+ * <p>Path: {route.path}</p>
61
+ * <p>Params: {JSON.stringify(route.params)}</p>
62
+ * <p>Query: {JSON.stringify(route.query)}</p>
63
+ * </div>
64
+ * );
65
+ * }
66
+ * ```
67
+ */
68
+ export declare function useRoute(): Route;
69
+ /**
70
+ * Get the current RouterView depth.
71
+ * Used internally by RouterView for nested routing.
72
+ *
73
+ * @returns Current depth level (0 for root, 1 for first nested, etc.)
74
+ *
75
+ * @example
76
+ * ```tsx
77
+ * import { useRouterViewDepth } from '@esmx/router-react';
78
+ *
79
+ * function DebugView() {
80
+ * const depth = useRouterViewDepth();
81
+ * return <div>Current depth: {depth}</div>;
82
+ * }
83
+ * ```
84
+ */
85
+ export declare function useRouterViewDepth(): number;
@@ -0,0 +1,23 @@
1
+ import { createContext, useContext } from "react";
2
+ export const RouterContext = createContext(null);
3
+ RouterContext.displayName = "RouterContext";
4
+ export const RouterViewDepthContext = createContext(0);
5
+ RouterViewDepthContext.displayName = "RouterViewDepthContext";
6
+ export function useRouterContext() {
7
+ const context = useContext(RouterContext);
8
+ if (!context) {
9
+ throw new Error(
10
+ "[@esmx/router-react] Router context not found. Please ensure your component is wrapped in a RouterProvider."
11
+ );
12
+ }
13
+ return context;
14
+ }
15
+ export function useRouter() {
16
+ return useRouterContext().router;
17
+ }
18
+ export function useRoute() {
19
+ return useRouterContext().route;
20
+ }
21
+ export function useRouterViewDepth() {
22
+ return useContext(RouterViewDepthContext);
23
+ }
@@ -0,0 +1,7 @@
1
+ export { RouterContext, RouterViewDepthContext, useRoute, useRouter, useRouterViewDepth } from './context';
2
+ export type { RouterLinkComponentProps } from './router-link';
3
+ export { RouterLink } from './router-link';
4
+ export { RouterProvider } from './router-provider';
5
+ export { RouterView } from './router-view';
6
+ export type { RouterContextValue, RouterProviderProps, RouterViewProps } from './types';
7
+ export { useLink } from './use-link';
package/dist/index.mjs ADDED
@@ -0,0 +1,11 @@
1
+ export {
2
+ RouterContext,
3
+ RouterViewDepthContext,
4
+ useRoute,
5
+ useRouter,
6
+ useRouterViewDepth
7
+ } from "./context.mjs";
8
+ export { RouterLink } from "./router-link.mjs";
9
+ export { RouterProvider } from "./router-provider.mjs";
10
+ export { RouterView } from "./router-view.mjs";
11
+ export { useLink } from "./use-link.mjs";
@@ -0,0 +1 @@
1
+ export {};