@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.
@@ -0,0 +1,104 @@
1
+ import type { Route } from '@esmx/router';
2
+ import { createElement, useCallback, useSyncExternalStore } from 'react';
3
+ import { RouterContext } from './context';
4
+ import type { RouterContextValue, RouterProviderProps } from './types';
5
+
6
+ /**
7
+ * RouterProvider component that provides router context to the React tree.
8
+ * This must wrap your application to enable routing functionality.
9
+ * Uses useSyncExternalStore for optimal React 18+ integration with concurrent features.
10
+ *
11
+ * @param props - Component props
12
+ * @param props.router - Router instance to provide
13
+ * @param props.children - Child components
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * import { Router, RouterMode } from '@esmx/router';
18
+ * import { RouterProvider } from '@esmx/router-react';
19
+ *
20
+ * const routes = [
21
+ * { path: '/', component: Home },
22
+ * { path: '/about', component: About }
23
+ * ];
24
+ *
25
+ * const router = new Router({
26
+ * routes,
27
+ * mode: RouterMode.history
28
+ * });
29
+ *
30
+ * function App() {
31
+ * return (
32
+ * <RouterProvider router={router}>
33
+ * <Layout>
34
+ * <RouterView />
35
+ * </Layout>
36
+ * </RouterProvider>
37
+ * );
38
+ * }
39
+ * ```
40
+ *
41
+ * @example
42
+ * ```tsx
43
+ * // SSR usage - initialize router with server URL
44
+ * import { Router, RouterMode } from '@esmx/router';
45
+ * import { RouterProvider } from '@esmx/router-react';
46
+ *
47
+ * const router = new Router({
48
+ * routes,
49
+ * mode: RouterMode.history,
50
+ * base: new URL(serverUrl)
51
+ * });
52
+ *
53
+ * function ServerApp() {
54
+ * return (
55
+ * <RouterProvider router={router}>
56
+ * <App />
57
+ * </RouterProvider>
58
+ * );
59
+ * }
60
+ * ```
61
+ */
62
+ export function RouterProvider({
63
+ router,
64
+ children
65
+ }: RouterProviderProps): React.ReactElement {
66
+ // Subscribe to route changes using useSyncExternalStore
67
+ // This ensures proper integration with React 18's concurrent features
68
+ const subscribe = useCallback(
69
+ (callback: () => void) => {
70
+ return router.afterEach(callback);
71
+ },
72
+ [router]
73
+ );
74
+
75
+ const getSnapshot = useCallback((): Route => {
76
+ return router.route;
77
+ }, [router]);
78
+
79
+ const getServerSnapshot = useCallback((): Route => {
80
+ return router.route;
81
+ }, [router]);
82
+
83
+ // Subscribe to route changes with useSyncExternalStore for concurrent mode safety
84
+ const route = useSyncExternalStore(
85
+ subscribe,
86
+ getSnapshot,
87
+ getServerSnapshot
88
+ );
89
+
90
+ // Create stable context value
91
+ const contextValue: RouterContextValue = {
92
+ router,
93
+ route
94
+ };
95
+
96
+ // Use createElement instead of JSX
97
+ return createElement(
98
+ RouterContext.Provider,
99
+ { value: contextValue },
100
+ children
101
+ );
102
+ }
103
+
104
+ RouterProvider.displayName = 'RouterProvider';
@@ -0,0 +1,113 @@
1
+ import {
2
+ type ComponentType,
3
+ createElement,
4
+ isValidElement,
5
+ type ReactElement,
6
+ useMemo
7
+ } from 'react';
8
+ import {
9
+ RouterViewDepthContext,
10
+ useRoute,
11
+ useRouterViewDepth
12
+ } from './context';
13
+ import type { RouterViewProps } from './types';
14
+ import { resolveComponent } from './util';
15
+
16
+ /**
17
+ * RouterView component that renders the matched route component.
18
+ * Acts as a placeholder where route components are rendered based on the current route.
19
+ * Supports nested routing with automatic depth tracking.
20
+ *
21
+ * @param props - Component props
22
+ * @param props.fallback - Optional fallback component when no route matches
23
+ *
24
+ * @example
25
+ * ```tsx
26
+ * // Basic usage
27
+ * import { RouterView } from '@esmx/router-react';
28
+ *
29
+ * function App() {
30
+ * return (
31
+ * <div>
32
+ * <nav>
33
+ * <RouterLink to="/">Home</RouterLink>
34
+ * <RouterLink to="/about">About</RouterLink>
35
+ * </nav>
36
+ * <RouterView />
37
+ * </div>
38
+ * );
39
+ * }
40
+ * ```
41
+ *
42
+ * @example
43
+ * ```tsx
44
+ * // Nested routing
45
+ * // Routes: [
46
+ * // { path: '/users', component: UsersLayout, children: [
47
+ * // { path: ':id', component: UserProfile }
48
+ * // ]}
49
+ * // ]
50
+ *
51
+ * function UsersLayout() {
52
+ * return (
53
+ * <div>
54
+ * <h1>Users</h1>
55
+ * <RouterView /> // Renders UserProfile for /users/:id
56
+ * </div>
57
+ * );
58
+ * }
59
+ * ```
60
+ *
61
+ * @example
62
+ * ```tsx
63
+ * // With fallback
64
+ * import { RouterView } from '@esmx/router-react';
65
+ *
66
+ * function App() {
67
+ * return (
68
+ * <RouterView fallback={<div>Page not found</div>} />
69
+ * );
70
+ * }
71
+ * ```
72
+ */
73
+ export function RouterView({ fallback }: RouterViewProps): ReactElement | null {
74
+ const route = useRoute();
75
+ const depth = useRouterViewDepth();
76
+
77
+ // Get the matched route at current depth
78
+ const matchedRoute = route.matched[depth];
79
+
80
+ // Resolve the component from the matched route
81
+ const Component = useMemo(() => {
82
+ if (!matchedRoute?.component) {
83
+ return null;
84
+ }
85
+ return resolveComponent(matchedRoute.component) as ComponentType<any>;
86
+ }, [matchedRoute?.component]);
87
+
88
+ // Render fallback if no component found
89
+ if (!Component) {
90
+ if (fallback) {
91
+ // If fallback is already a ReactElement, return it
92
+ if (isValidElement(fallback)) {
93
+ return fallback;
94
+ }
95
+ // If fallback is a component, render it
96
+ if (typeof fallback === 'function') {
97
+ return createElement(fallback as ComponentType);
98
+ }
99
+ // Return null for other cases (shouldn't happen with proper typing)
100
+ return null;
101
+ }
102
+ return null;
103
+ }
104
+
105
+ // Provide incremented depth for nested RouterViews using createElement
106
+ return createElement(
107
+ RouterViewDepthContext.Provider,
108
+ { value: depth + 1 },
109
+ createElement(Component, { key: matchedRoute.compilePath })
110
+ );
111
+ }
112
+
113
+ RouterView.displayName = 'RouterView';
package/src/types.ts ADDED
@@ -0,0 +1,30 @@
1
+ import type { Route, Router } from '@esmx/router';
2
+
3
+ /**
4
+ * Interface for the router context value.
5
+ * Contains the router instance and current route.
6
+ */
7
+ export interface RouterContextValue {
8
+ /** Router instance for navigation */
9
+ router: Router;
10
+ /** Current route object */
11
+ route: Route;
12
+ }
13
+
14
+ /**
15
+ * Props for the RouterProvider component.
16
+ */
17
+ export interface RouterProviderProps {
18
+ /** Router instance to provide to child components */
19
+ router: Router;
20
+ /** Child components */
21
+ children: React.ReactNode;
22
+ }
23
+
24
+ /**
25
+ * Props for the RouterView component.
26
+ */
27
+ export interface RouterViewProps {
28
+ /** Optional fallback component to render when no route matches */
29
+ fallback?: React.ComponentType | React.ReactNode;
30
+ }
@@ -0,0 +1,138 @@
1
+ import type { RouterLinkProps, RouterLinkResolved } from '@esmx/router';
2
+ import { useMemo } from 'react';
3
+ import { useRouter } from './context';
4
+
5
+ /**
6
+ * Hook to create reactive link helpers for custom navigation components.
7
+ * Returns a resolved link object with attributes, state, and event handlers.
8
+ *
9
+ * This hook is useful when you need to build custom link components
10
+ * with full control over rendering while retaining router functionality.
11
+ *
12
+ * @param props - RouterLink properties
13
+ * @returns Resolved link object with attributes, state, and navigation methods
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * import { useLink } from '@esmx/router-react';
18
+ *
19
+ * function CustomNavButton({ to, children }) {
20
+ * const link = useLink({ to, type: 'push', exact: 'include' });
21
+ *
22
+ * return (
23
+ * <button
24
+ * onClick={(e) => link.navigate(e)}
25
+ * className={link.isActive ? 'active' : ''}
26
+ * disabled={link.isExactActive}
27
+ * >
28
+ * {children}
29
+ * {link.isActive && <span>✓</span>}
30
+ * </button>
31
+ * );
32
+ * }
33
+ * ```
34
+ *
35
+ * @example
36
+ * ```tsx
37
+ * // Building a custom navigation card
38
+ * import { useLink } from '@esmx/router-react';
39
+ *
40
+ * interface NavCardProps {
41
+ * to: string;
42
+ * title: string;
43
+ * description: string;
44
+ * icon: React.ReactNode;
45
+ * }
46
+ *
47
+ * function NavCard({ to, title, description, icon }: NavCardProps) {
48
+ * const link = useLink({ to });
49
+ *
50
+ * return (
51
+ * <div
52
+ * className={`nav-card ${link.isActive ? 'active' : ''}`}
53
+ * onClick={(e) => link.navigate(e)}
54
+ * role="link"
55
+ * tabIndex={0}
56
+ * >
57
+ * <div className="icon">{icon}</div>
58
+ * <h3>{title}</h3>
59
+ * <p>{description}</p>
60
+ * {link.isExternal && <span className="external-badge">↗</span>}
61
+ * </div>
62
+ * );
63
+ * }
64
+ * ```
65
+ *
66
+ * @example
67
+ * ```tsx
68
+ * // Using all resolved properties
69
+ * import { useLink } from '@esmx/router-react';
70
+ *
71
+ * function DebugLink({ to }) {
72
+ * const link = useLink({ to, exact: 'exact' });
73
+ *
74
+ * return (
75
+ * <div>
76
+ * <a
77
+ * href={link.attributes.href}
78
+ * target={link.attributes.target}
79
+ * rel={link.attributes.rel}
80
+ * className={link.attributes.class}
81
+ * onClick={(e) => {
82
+ * e.preventDefault();
83
+ * link.navigate(e);
84
+ * }}
85
+ * >
86
+ * Link Text
87
+ * </a>
88
+ * <pre>
89
+ * isActive: {String(link.isActive)}
90
+ * isExactActive: {String(link.isExactActive)}
91
+ * isExternal: {String(link.isExternal)}
92
+ * type: {link.type}
93
+ * tag: {link.tag}
94
+ * </pre>
95
+ * </div>
96
+ * );
97
+ * }
98
+ * ```
99
+ */
100
+ export function useLink(props: RouterLinkProps): RouterLinkResolved {
101
+ const router = useRouter();
102
+ const {
103
+ to,
104
+ type,
105
+ replace,
106
+ exact,
107
+ activeClass,
108
+ event,
109
+ tag,
110
+ layerOptions,
111
+ beforeNavigate
112
+ } = props;
113
+
114
+ return useMemo(() => {
115
+ return router.resolveLink({
116
+ to,
117
+ type,
118
+ replace,
119
+ exact,
120
+ activeClass,
121
+ event,
122
+ tag,
123
+ layerOptions,
124
+ beforeNavigate
125
+ });
126
+ }, [
127
+ router,
128
+ to,
129
+ type,
130
+ replace,
131
+ exact,
132
+ activeClass,
133
+ event,
134
+ tag,
135
+ layerOptions,
136
+ beforeNavigate
137
+ ]);
138
+ }
package/src/util.ts ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Check if a value is an ES module.
3
+ * @param obj - The object to check
4
+ * @returns True if the object is an ES module
5
+ */
6
+ export function isESModule(obj: unknown): obj is Record<string | symbol, any> {
7
+ if (!obj || typeof obj !== 'object') return false;
8
+ const module = obj as Record<string | symbol, any>;
9
+ return (
10
+ Boolean(module.__esModule) || module[Symbol.toStringTag] === 'Module'
11
+ );
12
+ }
13
+
14
+ /**
15
+ * Resolve a component from potentially wrapped module format.
16
+ * Handles ES modules with default exports.
17
+ * @param component - The component to resolve
18
+ * @returns The resolved component
19
+ */
20
+ export function resolveComponent(component: unknown): unknown {
21
+ if (!component) return null;
22
+
23
+ if (isESModule(component)) {
24
+ return component.default || component;
25
+ }
26
+
27
+ if (
28
+ component &&
29
+ typeof component === 'object' &&
30
+ !Array.isArray(component) &&
31
+ 'default' in component &&
32
+ Object.keys(component).length === 1
33
+ ) {
34
+ return (component as { default: unknown }).default;
35
+ }
36
+
37
+ return component;
38
+ }