router-kit 1.3.4 → 2.0.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.
Files changed (49) hide show
  1. package/README.md +117 -421
  2. package/dist/components/Link.d.ts +23 -6
  3. package/dist/components/Link.js +51 -6
  4. package/dist/components/NavLink.d.ts +44 -7
  5. package/dist/components/NavLink.js +111 -10
  6. package/dist/components/Outlet.d.ts +66 -0
  7. package/dist/components/Outlet.js +69 -0
  8. package/dist/components/Router.d.ts +57 -11
  9. package/dist/components/Router.js +60 -12
  10. package/dist/components/route.d.ts +57 -7
  11. package/dist/components/route.js +35 -5
  12. package/dist/context/OutletContext.d.ts +41 -0
  13. package/dist/context/OutletContext.js +31 -0
  14. package/dist/context/RouterContext.d.ts +9 -0
  15. package/dist/context/RouterContext.js +21 -1
  16. package/dist/context/RouterProvider.d.ts +15 -4
  17. package/dist/context/RouterProvider.js +321 -84
  18. package/dist/core/createRouter.d.ts +65 -0
  19. package/dist/core/createRouter.js +126 -7
  20. package/dist/hooks/useBlocker.d.ts +65 -0
  21. package/dist/hooks/useBlocker.js +152 -0
  22. package/dist/hooks/useDynamicComponents.d.ts +61 -2
  23. package/dist/hooks/useDynamicComponents.js +89 -17
  24. package/dist/hooks/useLoaderData.d.ts +98 -0
  25. package/dist/hooks/useLoaderData.js +107 -0
  26. package/dist/hooks/useLocation.d.ts +37 -0
  27. package/dist/hooks/useLocation.js +106 -1
  28. package/dist/hooks/useMatches.d.ts +99 -0
  29. package/dist/hooks/useMatches.js +114 -0
  30. package/dist/hooks/useNavigate.d.ts +59 -0
  31. package/dist/hooks/useNavigate.js +70 -0
  32. package/dist/hooks/useParams.d.ts +57 -2
  33. package/dist/hooks/useParams.js +60 -14
  34. package/dist/hooks/useQuery.d.ts +53 -3
  35. package/dist/hooks/useQuery.js +107 -8
  36. package/dist/hooks/useRouter.d.ts +34 -0
  37. package/dist/hooks/useRouter.js +35 -1
  38. package/dist/index.d.ts +16 -6
  39. package/dist/index.js +21 -5
  40. package/dist/ssr/StaticRouter.d.ts +65 -0
  41. package/dist/ssr/StaticRouter.js +292 -0
  42. package/dist/ssr/hydrateRouter.d.ts +44 -0
  43. package/dist/ssr/hydrateRouter.js +60 -0
  44. package/dist/ssr/index.d.ts +92 -0
  45. package/dist/ssr/index.js +92 -0
  46. package/dist/ssr/serverUtils.d.ts +107 -0
  47. package/dist/ssr/serverUtils.js +263 -0
  48. package/dist/types/index.d.ts +201 -2
  49. package/package.json +14 -2
@@ -4,14 +4,44 @@
4
4
  *
5
5
  * @example
6
6
  * ```tsx
7
- * <Route path="/users/:id" element={<UserProfile />} />
8
- * <Route path={["/about", "/about-us"]} element={<About />} />
9
- * <Route path="/dashboard" element={<Dashboard />}>
10
- * <Route path="settings" element={<Settings />} />
7
+ * // Basic route
8
+ * <Route path="/users/:id" component={<UserProfile />} />
9
+ *
10
+ * // Multiple paths
11
+ * <Route path={["/about", "/about-us"]} component={<About />} />
12
+ *
13
+ * // Nested routes
14
+ * <Route path="/dashboard" component={<Dashboard />}>
15
+ * <Route path="settings" component={<Settings />} />
16
+ * <Route path="profile" component={<Profile />} />
11
17
  * </Route>
18
+ *
19
+ * // With loader
20
+ * <Route
21
+ * path="/user/:id"
22
+ * component={<UserPage />}
23
+ * loader={async ({ params }) => fetchUser(params.id)}
24
+ * />
25
+ *
26
+ * // With guard
27
+ * <Route
28
+ * path="/admin"
29
+ * component={<AdminPanel />}
30
+ * guard={() => isAdmin() || '/login'}
31
+ * />
32
+ *
33
+ * // With metadata
34
+ * <Route
35
+ * path="/about"
36
+ * component={<About />}
37
+ * meta={{ title: 'About Us', description: 'Learn about us' }}
38
+ * />
39
+ *
40
+ * // Catch-all route
41
+ * <Route path="*" component={<NotFound />} />
12
42
  * ```
13
43
  */
14
- export function Route(props) {
44
+ export function Route(_props) {
15
45
  // This component doesn't render anything directly
16
46
  // It's used as a declarative way to define routes that will be processed by Router
17
47
  return null;
@@ -0,0 +1,41 @@
1
+ import { ReactNode } from "react";
2
+ import type { Route, RouteMatch } from "../types";
3
+ /**
4
+ * Outlet context for nested route rendering
5
+ */
6
+ export interface OutletContextType {
7
+ /** Current outlet content to render */
8
+ outlet: ReactNode;
9
+ /** Remaining child routes */
10
+ childRoutes: Route[];
11
+ /** Current route matches */
12
+ matches: RouteMatch[];
13
+ /** Current depth in route tree */
14
+ depth: number;
15
+ /** Custom context data passed to outlet */
16
+ context?: unknown;
17
+ }
18
+ /**
19
+ * Context for outlet data
20
+ */
21
+ export declare const OutletDataContext: import("react").Context<OutletContextType | null>;
22
+ /**
23
+ * Hook to access outlet context
24
+ */
25
+ export declare function useOutletContext<T = unknown>(): T;
26
+ /**
27
+ * Hook to check if there's an outlet available
28
+ */
29
+ export declare function useOutlet(): ReactNode;
30
+ /**
31
+ * Provider for outlet context
32
+ */
33
+ export declare function OutletProvider({ children, outlet, childRoutes, matches, depth, context, }: {
34
+ children: ReactNode;
35
+ outlet: ReactNode;
36
+ childRoutes?: Route[];
37
+ matches?: RouteMatch[];
38
+ depth?: number;
39
+ context?: unknown;
40
+ }): import("react/jsx-runtime").JSX.Element;
41
+ export default OutletDataContext;
@@ -0,0 +1,31 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext } from "react";
3
+ /**
4
+ * Context for outlet data
5
+ */
6
+ export const OutletDataContext = createContext(null);
7
+ /**
8
+ * Hook to access outlet context
9
+ */
10
+ export function useOutletContext() {
11
+ const context = useContext(OutletDataContext);
12
+ if (!context) {
13
+ throw new Error("useOutletContext must be used within a route component");
14
+ }
15
+ return context.context;
16
+ }
17
+ /**
18
+ * Hook to check if there's an outlet available
19
+ */
20
+ export function useOutlet() {
21
+ var _a;
22
+ const context = useContext(OutletDataContext);
23
+ return (_a = context === null || context === void 0 ? void 0 : context.outlet) !== null && _a !== void 0 ? _a : null;
24
+ }
25
+ /**
26
+ * Provider for outlet context
27
+ */
28
+ export function OutletProvider({ children, outlet, childRoutes = [], matches = [], depth = 0, context, }) {
29
+ return (_jsx(OutletDataContext.Provider, { value: { outlet, childRoutes, matches, depth, context }, children: children }));
30
+ }
31
+ export default OutletDataContext;
@@ -1,3 +1,12 @@
1
1
  import type { RouterContextType } from "../types";
2
+ /**
3
+ * Router context - provides routing state and navigation functions
4
+ * throughout the application
5
+ */
2
6
  declare const RouterContext: import("react").Context<RouterContextType | undefined>;
7
+ /**
8
+ * Internal hook to access router context with validation
9
+ * @internal
10
+ */
11
+ export declare function useRouterContext(): RouterContextType;
3
12
  export default RouterContext;
@@ -1,3 +1,23 @@
1
- import { createContext } from "react";
1
+ import { createContext, useContext } from "react";
2
+ /**
3
+ * Router context - provides routing state and navigation functions
4
+ * throughout the application
5
+ */
2
6
  const RouterContext = createContext(undefined);
7
+ /**
8
+ * Display name for debugging
9
+ */
10
+ RouterContext.displayName = "RouterContext";
11
+ /**
12
+ * Internal hook to access router context with validation
13
+ * @internal
14
+ */
15
+ export function useRouterContext() {
16
+ const context = useContext(RouterContext);
17
+ if (context === undefined) {
18
+ throw new Error("[router-kit] useRouter must be used within a RouterProvider. " +
19
+ "Wrap your application with <RouterProvider> or use createRouter().");
20
+ }
21
+ return context;
22
+ }
3
23
  export default RouterContext;
@@ -1,5 +1,16 @@
1
- import type { Route } from "../types";
2
- declare const RouterProvider: ({ routes }: {
3
- routes: Route[];
4
- }) => import("react/jsx-runtime").JSX.Element;
1
+ import type { RouterProviderProps } from "../types";
2
+ /**
3
+ * RouterProvider - Professional-grade router provider component
4
+ *
5
+ * Features:
6
+ * - Static and dynamic route matching with priority
7
+ * - Route params extraction
8
+ * - Nested routes support
9
+ * - Route guards and redirects
10
+ * - Loader data support
11
+ * - Navigation transitions
12
+ * - Scroll restoration
13
+ * - History management
14
+ */
15
+ declare const RouterProvider: ({ routes, basename, fallbackElement, }: RouterProviderProps) => import("react/jsx-runtime").JSX.Element;
5
16
  export default RouterProvider;
@@ -1,9 +1,13 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useEffect, useState } from "react";
2
+ import { Suspense, useCallback, useEffect, useMemo, useRef, useState, useTransition, } from "react";
3
3
  import join from "url-join";
4
4
  import Page404 from "../pages/404";
5
5
  import { createRouterError, RouterErrorCode, RouterErrors, } from "../utils/error/errors";
6
+ import { OutletProvider } from "./OutletContext";
6
7
  import RouterContext from "./RouterContext";
8
+ /**
9
+ * Validates a URL string
10
+ */
7
11
  const validateUrl = (url) => {
8
12
  try {
9
13
  new URL(url, window.location.origin);
@@ -13,101 +17,218 @@ const validateUrl = (url) => {
13
17
  return false;
14
18
  }
15
19
  };
16
- const RouterProvider = ({ routes }) => {
17
- const [path, setPath] = useState("");
18
- const [fullPathWithParams, setFullPathWithParams] = useState("");
19
- let page404 = null;
20
- useEffect(() => {
21
- setPath(window.location.pathname);
22
- const patchHistory = (method) => {
23
- const original = window.history[method];
24
- return function (state, title, url) {
25
- const result = original.apply(this, [state, title, url]);
26
- window.dispatchEvent(new Event("locationchange"));
27
- return result;
28
- };
29
- };
30
- const originalPush = window.history.pushState;
31
- const originalReplace = window.history.replaceState;
32
- window.history.pushState = patchHistory("pushState");
33
- window.history.replaceState = patchHistory("replaceState");
34
- const handleLocationChange = () => {
35
- setPath(window.location.pathname);
36
- };
37
- window.addEventListener("popstate", handleLocationChange);
38
- window.addEventListener("locationchange", handleLocationChange);
39
- return () => {
40
- window.history.pushState = originalPush;
41
- window.history.replaceState = originalReplace;
42
- window.removeEventListener("popstate", handleLocationChange);
43
- window.removeEventListener("locationchange", handleLocationChange);
20
+ /**
21
+ * Creates a unique key for location tracking
22
+ */
23
+ const createKey = () => {
24
+ return Math.random().toString(36).substring(2, 10);
25
+ };
26
+ /**
27
+ * Gets current location from window
28
+ */
29
+ const getCurrentLocation = () => {
30
+ if (typeof window === "undefined") {
31
+ return {
32
+ pathname: "/",
33
+ search: "",
34
+ hash: "",
35
+ state: null,
36
+ key: "default",
44
37
  };
45
- }, []);
46
- const pathValidation = (routeFullPath, currentPath) => {
47
- const routePaths = routeFullPath.split("|");
48
- for (const routePath of routePaths) {
49
- const routeParts = routePath.split("/").filter(Boolean);
50
- const pathParts = currentPath.split("/").filter(Boolean);
51
- if (routeParts.length !== pathParts.length)
52
- continue;
53
- let isMatch = true;
54
- for (let i = 0; i < routeParts.length; i++) {
55
- const r = routeParts[i];
56
- const p = pathParts[i];
57
- if (r.startsWith(":"))
58
- continue;
59
- if (r !== p) {
60
- isMatch = false;
61
- break;
62
- }
38
+ }
39
+ return {
40
+ pathname: window.location.pathname,
41
+ search: window.location.search,
42
+ hash: window.location.hash,
43
+ state: window.history.state,
44
+ key: createKey(),
45
+ };
46
+ };
47
+ /**
48
+ * Extracts params from a path using a pattern
49
+ */
50
+ const extractParams = (pattern, pathname) => {
51
+ const patternParts = pattern.split("/").filter(Boolean);
52
+ const pathParts = pathname.split("/").filter(Boolean);
53
+ if (patternParts.length !== pathParts.length) {
54
+ // Check for catch-all pattern
55
+ const hasCatchAll = patternParts.some((p) => p.startsWith("*"));
56
+ if (!hasCatchAll)
57
+ return null;
58
+ }
59
+ const params = {};
60
+ for (let i = 0; i < patternParts.length; i++) {
61
+ const patternPart = patternParts[i];
62
+ const pathPart = pathParts[i];
63
+ // Catch-all segment (*splat or **)
64
+ if (patternPart.startsWith("*")) {
65
+ const paramName = patternPart.slice(1) || "splat";
66
+ params[paramName] = pathParts.slice(i).join("/");
67
+ return params;
68
+ }
69
+ // Dynamic segment (:param)
70
+ if (patternPart.startsWith(":")) {
71
+ const paramName = patternPart.slice(1);
72
+ // Handle optional params (:param?)
73
+ if (paramName.endsWith("?")) {
74
+ params[paramName.slice(0, -1)] = pathPart !== null && pathPart !== void 0 ? pathPart : "";
75
+ }
76
+ else {
77
+ if (pathPart === undefined)
78
+ return null;
79
+ params[paramName] = pathPart;
63
80
  }
64
- if (isMatch)
65
- return routePath;
81
+ continue;
66
82
  }
67
- return false;
68
- };
69
- const getComponent = (routesList, currentPath, parentPath = "/") => {
83
+ // Static segment - must match exactly
84
+ if (patternPart !== pathPart)
85
+ return null;
86
+ }
87
+ return params;
88
+ };
89
+ /**
90
+ * RouterProvider - Professional-grade router provider component
91
+ *
92
+ * Features:
93
+ * - Static and dynamic route matching with priority
94
+ * - Route params extraction
95
+ * - Nested routes support
96
+ * - Route guards and redirects
97
+ * - Loader data support
98
+ * - Navigation transitions
99
+ * - Scroll restoration
100
+ * - History management
101
+ */
102
+ const RouterProvider = ({ routes, basename = "", fallbackElement, }) => {
103
+ const [location, setLocation] = useState(getCurrentLocation);
104
+ const [pattern, setPattern] = useState("");
105
+ const [params, setParams] = useState({});
106
+ const [matches, setMatches] = useState([]);
107
+ const [loaderData, setLoaderData] = useState(null);
108
+ const [meta, setMeta] = useState(null);
109
+ const [isPending, startTransition] = useTransition();
110
+ const page404Ref = useRef(null);
111
+ const scrollPositions = useRef(new Map());
112
+ const isNavigatingRef = useRef(false);
113
+ /**
114
+ * Normalize pathname by removing basename
115
+ */
116
+ const normalizePathname = useCallback((pathname) => {
117
+ if (basename && pathname.startsWith(basename)) {
118
+ return pathname.slice(basename.length) || "/";
119
+ }
120
+ return pathname;
121
+ }, [basename]);
122
+ /**
123
+ * Match a single path pattern against current pathname
124
+ */
125
+ const matchPath = useCallback((routePattern, currentPath) => {
126
+ const patterns = routePattern.split("|");
127
+ for (const pat of patterns) {
128
+ const extractedParams = extractParams(pat, currentPath);
129
+ if (extractedParams !== null) {
130
+ return { match: true, params: extractedParams, pattern: pat };
131
+ }
132
+ }
133
+ return null;
134
+ }, []);
135
+ /**
136
+ * Get component and match info from routes
137
+ */
138
+ const getComponent = useCallback((routesList, currentPath, parentPath = "/") => {
70
139
  const staticRoutes = [];
71
140
  const dynamicRoutes = [];
141
+ const catchAllRoutes = [];
72
142
  for (const route of routesList) {
73
143
  const is404 = route.path === "404" || route.path === "/404";
74
144
  if (is404) {
75
- page404 = route.component;
145
+ page404Ref.current = route.component;
76
146
  continue;
77
147
  }
78
148
  const pathArray = Array.isArray(route.path) ? route.path : [route.path];
149
+ const hasCatchAll = pathArray.some((p) => p.includes("*"));
79
150
  const hasDynamicParams = pathArray.some((p) => p.includes(":"));
80
- if (hasDynamicParams) {
151
+ if (hasCatchAll) {
152
+ catchAllRoutes.push(route);
153
+ }
154
+ else if (hasDynamicParams) {
81
155
  dynamicRoutes.push(route);
82
156
  }
83
157
  else {
84
158
  staticRoutes.push(route);
85
159
  }
86
160
  }
87
- for (const route of staticRoutes) {
161
+ // Priority: static > dynamic > catch-all
162
+ const orderedRoutes = [
163
+ ...staticRoutes,
164
+ ...dynamicRoutes,
165
+ ...catchAllRoutes,
166
+ ];
167
+ for (const route of orderedRoutes) {
88
168
  const fullPath = join(parentPath, `/${route.path}`);
89
- const matchedPath = pathValidation(fullPath, currentPath);
90
- if (matchedPath) {
91
- if (matchedPath !== fullPathWithParams) {
92
- setFullPathWithParams(matchedPath);
169
+ const matchResult = matchPath(fullPath, currentPath);
170
+ if (matchResult) {
171
+ // Handle redirects
172
+ if (route.redirectTo) {
173
+ // Schedule redirect in next tick to avoid state update during render
174
+ setTimeout(() => navigate(route.redirectTo), 0);
175
+ return null;
93
176
  }
94
- return route.component;
95
- }
96
- if (route.children) {
97
- const childMatch = getComponent(route.children, currentPath, fullPath);
98
- if (childMatch)
99
- return childMatch;
100
- }
101
- }
102
- for (const route of dynamicRoutes) {
103
- const fullPath = join(parentPath, `/${route.path}`);
104
- const matchedPath = pathValidation(fullPath, currentPath);
105
- if (matchedPath) {
106
- if (matchedPath !== fullPathWithParams) {
107
- setFullPathWithParams(matchedPath);
177
+ // Handle guards
178
+ if (route.guard) {
179
+ const guardResult = route.guard({
180
+ pathname: currentPath,
181
+ params: matchResult.params,
182
+ search: location.search,
183
+ });
184
+ if (typeof guardResult === "string") {
185
+ setTimeout(() => navigate(guardResult), 0);
186
+ return null;
187
+ }
188
+ if (guardResult === false) {
189
+ continue; // Skip this route
190
+ }
191
+ }
192
+ // Update matches state
193
+ const newMatch = {
194
+ route,
195
+ params: matchResult.params,
196
+ pathname: currentPath,
197
+ pathnameBase: parentPath,
198
+ pattern: matchResult.pattern,
199
+ };
200
+ if (pattern !== matchResult.pattern) {
201
+ setPattern(matchResult.pattern);
202
+ }
203
+ if (JSON.stringify(params) !== JSON.stringify(matchResult.params)) {
204
+ setParams(matchResult.params);
205
+ }
206
+ setMatches((prev) => [...prev, newMatch]);
207
+ // Handle route meta
208
+ if (route.meta) {
209
+ setMeta(route.meta);
210
+ if (route.meta.title && typeof document !== "undefined") {
211
+ document.title = route.meta.title;
212
+ }
213
+ }
214
+ // Handle loader
215
+ if (route.loader) {
216
+ const abortController = new AbortController();
217
+ Promise.resolve(route.loader({
218
+ params: matchResult.params,
219
+ request: new Request(window.location.href),
220
+ signal: abortController.signal,
221
+ })).then(setLoaderData);
222
+ }
223
+ // Handle nested routes with Outlet support
224
+ if (route.children && route.children.length > 0) {
225
+ const childComponent = getComponent(route.children, currentPath, fullPath);
226
+ // Wrap parent component with OutletProvider to render children via Outlet
227
+ return (_jsx(OutletProvider, { outlet: childComponent, childRoutes: route.children, matches: matches, depth: parentPath.split("/").filter(Boolean).length, children: route.component }));
108
228
  }
109
229
  return route.component;
110
230
  }
231
+ // Check children routes (for routes without matching parent)
111
232
  if (route.children) {
112
233
  const childMatch = getComponent(route.children, currentPath, fullPath);
113
234
  if (childMatch)
@@ -115,32 +236,148 @@ const RouterProvider = ({ routes }) => {
115
236
  }
116
237
  }
117
238
  return null;
118
- };
119
- const navigate = (to, options) => {
239
+ }, [location.search, matchPath, params, pattern]);
240
+ /**
241
+ * Navigate to a new location
242
+ */
243
+ const navigate = useCallback((to, options) => {
244
+ // Handle numeric (delta) navigation
245
+ if (typeof to === "number") {
246
+ window.history.go(to);
247
+ return;
248
+ }
249
+ // Normalize path
250
+ let targetPath = to;
120
251
  if (!/^https?:\/\//i.test(to)) {
121
- to = to.startsWith("/") ? to : `/${to}`;
252
+ targetPath = to.startsWith("/") ? to : `/${to}`;
253
+ if (basename) {
254
+ targetPath = join(basename, targetPath);
255
+ }
122
256
  }
123
- if (!validateUrl(to)) {
257
+ // Validate URL
258
+ if (!validateUrl(targetPath)) {
124
259
  RouterErrors.invalidRoute(to, "Invalid URL format");
125
260
  return;
126
261
  }
127
262
  try {
263
+ // Save scroll position before navigation
264
+ if (!(options === null || options === void 0 ? void 0 : options.preventScrollReset)) {
265
+ scrollPositions.current.set(location.key, window.scrollY);
266
+ }
267
+ isNavigatingRef.current = true;
128
268
  if (options === null || options === void 0 ? void 0 : options.replace) {
129
- window.history.replaceState((options === null || options === void 0 ? void 0 : options.state) || {}, "", to);
269
+ window.history.replaceState({ ...options === null || options === void 0 ? void 0 : options.state, key: createKey() }, "", targetPath);
130
270
  }
131
271
  else {
132
- window.history.pushState((options === null || options === void 0 ? void 0 : options.state) || {}, "", to);
272
+ window.history.pushState({ ...options === null || options === void 0 ? void 0 : options.state, key: createKey() }, "", targetPath);
273
+ }
274
+ // Use transition for better UX
275
+ startTransition(() => {
276
+ setLocation(getCurrentLocation());
277
+ setMatches([]); // Reset matches for new route
278
+ });
279
+ // Scroll to top unless prevented
280
+ if (!(options === null || options === void 0 ? void 0 : options.preventScrollReset)) {
281
+ window.scrollTo(0, 0);
133
282
  }
134
- setPath(to);
135
283
  }
136
284
  catch (error) {
137
285
  const navError = createRouterError(RouterErrorCode.NAVIGATION_ABORTED, `Navigation to "${to}" failed: ${error instanceof Error ? error.message : String(error)}`, { to, error });
138
286
  console.error(navError.toConsoleMessage());
139
287
  throw navError;
140
288
  }
141
- };
142
- const matchedComponent = getComponent(routes, path);
143
- const component = matchedComponent !== null && matchedComponent !== void 0 ? matchedComponent : (page404 || _jsx(Page404, {}));
144
- return (_jsx(RouterContext.Provider, { value: { path, fullPathWithParams, navigate }, children: component }));
289
+ finally {
290
+ isNavigatingRef.current = false;
291
+ }
292
+ }, [basename, location.key]);
293
+ /**
294
+ * Go back in history
295
+ */
296
+ const back = useCallback(() => {
297
+ window.history.back();
298
+ }, []);
299
+ /**
300
+ * Go forward in history
301
+ */
302
+ const forward = useCallback(() => {
303
+ window.history.forward();
304
+ }, []);
305
+ /**
306
+ * Setup history listeners
307
+ */
308
+ useEffect(() => {
309
+ const handleLocationChange = () => {
310
+ startTransition(() => {
311
+ setLocation(getCurrentLocation());
312
+ setMatches([]); // Reset matches for new route
313
+ });
314
+ };
315
+ // Patch history methods to dispatch custom event
316
+ const patchHistory = (method) => {
317
+ const original = window.history[method];
318
+ return function (state, title, url) {
319
+ const result = original.apply(this, [state, title, url]);
320
+ window.dispatchEvent(new CustomEvent("locationchange", {
321
+ detail: { action: method === "pushState" ? "PUSH" : "REPLACE" },
322
+ }));
323
+ return result;
324
+ };
325
+ };
326
+ const originalPush = window.history.pushState;
327
+ const originalReplace = window.history.replaceState;
328
+ window.history.pushState = patchHistory("pushState");
329
+ window.history.replaceState = patchHistory("replaceState");
330
+ window.addEventListener("popstate", handleLocationChange);
331
+ window.addEventListener("locationchange", handleLocationChange);
332
+ return () => {
333
+ window.history.pushState = originalPush;
334
+ window.history.replaceState = originalReplace;
335
+ window.removeEventListener("popstate", handleLocationChange);
336
+ window.removeEventListener("locationchange", handleLocationChange);
337
+ };
338
+ }, []);
339
+ /**
340
+ * Compute matched component
341
+ */
342
+ const normalizedPath = normalizePathname(location.pathname);
343
+ const matchedComponent = useMemo(() => getComponent(routes, normalizedPath), [routes, normalizedPath, getComponent]);
344
+ const component = matchedComponent !== null && matchedComponent !== void 0 ? matchedComponent : (page404Ref.current || _jsx(Page404, {}));
345
+ /**
346
+ * Build context value with memoization
347
+ */
348
+ const contextValue = useMemo(() => ({
349
+ // New API
350
+ pathname: normalizedPath,
351
+ pattern,
352
+ search: location.search,
353
+ hash: location.hash,
354
+ state: location.state,
355
+ params,
356
+ matches,
357
+ navigate,
358
+ back,
359
+ forward,
360
+ isNavigating: isPending || isNavigatingRef.current,
361
+ loaderData,
362
+ meta,
363
+ // Legacy aliases for backward compatibility
364
+ path: normalizedPath,
365
+ fullPathWithParams: pattern,
366
+ }), [
367
+ normalizedPath,
368
+ pattern,
369
+ location.search,
370
+ location.hash,
371
+ location.state,
372
+ params,
373
+ matches,
374
+ navigate,
375
+ back,
376
+ forward,
377
+ isPending,
378
+ loaderData,
379
+ meta,
380
+ ]);
381
+ return (_jsx(RouterContext.Provider, { value: contextValue, children: fallbackElement && isPending ? (_jsx(Suspense, { fallback: fallbackElement, children: component })) : (component) }));
145
382
  };
146
383
  export default RouterProvider;