clear-react-router 1.7.1 → 1.7.3

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 CHANGED
@@ -1,17 +1,23 @@
1
1
  [![npm version](https://badge.fury.io/js/clear-react-router.svg)](https://www.npmjs.com/package/clear-react-router)
2
2
 
3
- A lightweight, type-safe routing library for React applications with nested routes, data loading, navigation blocking, and prefetching.
3
+ # Clear Router
4
+
5
+ A lightweight, type-safe routing library for React applications with nested routes, data loading, navigation blocking, prefetching, and route actions.
4
6
 
5
7
  ## Why Clear Router?
6
8
 
7
- Most React routers focus on flexibility and ecosystem integrations.
8
- Clear Router focuses on predictable navigation with a small, explicit API.
9
+ Most React routers focus on flexibility and ecosystem integrations. Clear Router focuses on predictable navigation with a small, explicit API and minimal setup.
10
+
11
+ There is no `RouterProvider` or provider hierarchy to manage. Simply render `<Router />` once, and use router hooks anywhere in your application.
9
12
 
10
13
  It provides first-class support for:
11
14
 
12
- - Predictable routing
13
- - Built-in data loading
14
- - Small API
15
+ * Predictable routing
16
+ * Built-in data loading
17
+ * Route actions and forms
18
+ * Simple, provider-free architecture
19
+ * Small, explicit API
20
+
15
21
 
16
22
  ## Features
17
23
 
@@ -44,43 +50,20 @@ Normalizes route configuration. Handles wildcard `*` routes, extracts dynamic pa
44
50
  | `loader` | `({ params, context, setContext }) => Promise<unknown>` | Fetch data using route params and context. Can update context via `setContext` |
45
51
  | `afterLoad` | `({ params, context, setContext }) => Promise<void>` | Analytics, side effects after data is loaded. Can update context via `setContext` |
46
52
  | `fallback` | `ReactElement \| () => ReactElement` | Loading fallback (for lazy loading) |
47
- | `loaderFallback` | `ReactElement \| () => ReactElement` | Loading fallback (for loader) |
48
- | `errorElement` | `ReactElement \| () => ReactElement` | Error fallback |
53
+ | `loaderFallback` | `ReactElement \| () => ReactElement` | Loading fallback for the route's `loader`. Overrides the global `defaultLoaderFallback` set in `Router` |
54
+ | `errorElement` | `ReactElement \| () => ReactElement` | Error fallback for the route. Overrides the global `defaultErrorElement` set in `Router` |
49
55
  | `staleTime` | `number` | Time in ms before cached data is considered stale and re-fetched in the background. If not provided, data never expires (cached forever) |
50
56
  | `actions` | `({ params, context, invalidate, setContext }) => Record<string, (formData: FormData) => unknown \| Promise<unknown>>` | Defines route actions for data mutations. Actions receive `FormData`, can update context via `setContext`, and can refresh loader data using the router-provided `invalidate`. |
51
57
 
52
-
53
- ### `RouterProvider`
54
-
55
- The root component that provides routing context to the application. Place static UI elements (like navbar or footer) outside `<Router />` to prevent unnecessary re-renders.
56
-
57
- | Prop | Type | Default | Description |
58
- |------|------|---------|-------------|
59
- | `routes` | `RouteItem[]` | required | Array of route configurations |
60
- | `children` | `ReactNode` | required | App content (must include `<Router />`) |
61
-
62
- ```tsx
63
- function App() {
64
- return (
65
- <RouterProvider routeList={routes}>
66
- <Navbar /> {/* Static */}
67
- <main>
68
- <Router isAnimated animationDuration={800} /> {/* Dynamic — renders current page */}
69
- </main>
70
- <Footer /> {/* Static */}
71
- </RouterProvider>
72
- );
73
- }
74
- ```
75
-
76
58
  ### `Router`
77
59
 
78
- Renders the current route's component. Must be placed inside `<RouterProvider>`.
79
-
80
60
  | Prop | Type | Default | Description |
81
61
  |------|------|---------|-------------|
62
+ | `routes` | `RouteItem[]` | required | Array of route configurations |
82
63
  | `isAnimated` | `boolean \| undefined` | `false` | Enable smooth page fade transitions |
83
64
  | `animationDuration` | `number` | `optional` | Animation duration in milliseconds (browser default is used if not set) |
65
+ | `defaultLoaderFallback` | `ReactElement \| () => ReactElement` | `optional` | Default loading fallback for every route loader |
66
+ | `defaultErrorElement` | `ReactElement \| () => ReactElement` | `optional` | Default error fallback for every route |
84
67
  | `spinner` | `boolean \| undefined` | `true` | Show a small spinner in the corner while loading data (only when `isAnimated` is enabled) |
85
68
  | `preserveScroll` | `boolean \| undefined` | `true` | Save and restore scroll position when navigating between pages |
86
69
  | `showFallbackOnAnimation` | `boolean \| undefined` | `false` | Show `loaderFallback` even when `isAnimated` is `true` (instead of spinner) |
@@ -90,10 +73,10 @@ Renders the current route's component. Must be placed inside `<RouterProvider>`.
90
73
  | `errorBoundary` | `ComponentType<{ children: ReactNode }>` | `undefined` | Custom error boundary component for catching render errors in route components |
91
74
 
92
75
  ```tsx
93
- <RouterProvider routes={routes}>
76
+ <div>
94
77
  <Navbar />
95
- <Router spinner={false} isAnimated /> {/* disable the spinner */}
96
- </RouterProvider>
78
+ <Router routes={routes} spinner={false} isAnimated /> {/* disable the spinner */}
79
+ </div>
97
80
  ```
98
81
 
99
82
  > **Note:** When `isAnimated` is enabled, `loaderFallback` is not shown. Instead, a small spinner appears (if `spinner={true}`). On the initial page load, however, the route's loaderFallback is rendered if available.
@@ -121,12 +104,10 @@ Component for client-side navigation with prefetch support.
121
104
  **Example:**
122
105
 
123
106
  ```tsx
124
- import { RouterProvider, Router, Link } from 'clear-react-router';
107
+ import { Router, Link } from 'clear-react-router';
125
108
 
126
109
  // Global prefetch: hover with 100ms delay
127
- <RouterProvider routes={routes} prefetch="hover" hoverPrefetchDelay={100}>
128
- <Router />
129
- </RouterProvider>
110
+ <Router routes={routes} prefetch="hover" hoverPrefetchDelay={100} />
130
111
 
131
112
  // Override for a specific link
132
113
  <Link to="/heavy-page" prefetch="viewport">
@@ -334,15 +315,11 @@ This hook is useful when the mutation is triggered programmatically, such as fro
334
315
  You can provide a custom error boundary to catch rendering errors in route components. This is useful for preventing the entire app from crashing when a specific route fails to render.
335
316
 
336
317
  ```tsx
337
- import { RouterProvider, Router } from 'clear-react-router';
318
+ import { Router } from 'clear-react-router';
338
319
  import { routes } from './routes';
339
320
  import { ErrorBoundary } from './components/ErrorBoundary';
340
321
 
341
- const App = () => (
342
- <RouterProvider routes={routes}>
343
- <Router errorBoundary={ErrorBoundary} />
344
- </RouterProvider>
345
- );
322
+ const App = () => <Router routes={routes} errorBoundary={ErrorBoundary} />
346
323
  ```
347
324
  **Note:** The `errorBoundary` prop only catches render-time errors in route components. It does not catch errors in `loader` or `beforeLoad` — those are handled by the router's `errorElement` mechanism.
348
325
 
@@ -1,2 +1,2 @@
1
1
  import { RouterProps } from '../types/global';
2
- export declare const Router: ({ isAnimated, animationDuration, spinner, preserveScroll, showFallbackOnAnimation, prefetch, hoverPrefetchDelay, errorBoundary: ErrorBoundary, context: initialContext, }: RouterProps) => import("react/jsx-runtime").JSX.Element | null;
2
+ export declare const Router: ({ routes, animationDuration, isAnimated, spinner, preserveScroll, showFallbackOnAnimation, prefetch, hoverPrefetchDelay, errorBoundary: ErrorBoundary, context: initialContext, defaultLoaderFallback, defaultErrorElement, }: RouterProps) => import("react/jsx-runtime").JSX.Element | null;
@@ -1,7 +1,5 @@
1
1
  import { RouterProps } from '../types/global';
2
2
  declare class RouterConfig {
3
- isAnimated: boolean;
4
- showFallbackOnAnimation: boolean;
5
3
  prefetch: RouterProps['prefetch'];
6
4
  hoverPrefetchDelay: number;
7
5
  configure(config: Partial<RouterConfig>): void;
@@ -8,6 +8,8 @@ type UseHandleNavigation = {
8
8
  pathname: string;
9
9
  }): boolean;
10
10
  loaderStateRef: RefObject<LoaderState>;
11
+ isAnimated: boolean;
12
+ showFallbackOnAnimation: boolean;
11
13
  };
12
- export declare const useNavigation: ({ routes, revalidateCache, isCacheItemFresh, loaderStateRef }: UseHandleNavigation) => (nextLocation: Location) => Promise<void>;
14
+ export declare const useNavigation: ({ routes, revalidateCache, isCacheItemFresh, loaderStateRef, isAnimated, showFallbackOnAnimation: showFallback, }: UseHandleNavigation) => (nextLocation: Location) => Promise<void>;
13
15
  export {};
@@ -0,0 +1,5 @@
1
+ export declare const useRuntime: () => {
2
+ updateLocation: (route: import("..").Location) => Promise<void>;
3
+ prefetchLoader: (arg: string) => Promise<void>;
4
+ invalidate: (path?: string) => Promise<void>;
5
+ };
@@ -1,2 +1,2 @@
1
1
  import { RouterProps } from '../types/global';
2
- export declare const useSetRouterConfig: (routerProps: RouterProps) => void;
2
+ export declare const useSetRouterConfig: (routerProps: Omit<RouterProps, "routes">) => void;
@@ -0,0 +1,7 @@
1
+ type UseSetRuntime = {
2
+ updateLocation(arg: Location): Promise<void>;
3
+ prefetchLoader(arg: string): Promise<void>;
4
+ invalidate(arg?: string): Promise<void>;
5
+ };
6
+ export declare const useSetRouterRuntime: ({ updateLocation, prefetchLoader, invalidate }: UseSetRuntime) => void;
7
+ export {};
package/dist/index.d.ts CHANGED
@@ -1,4 +1,3 @@
1
- export { RouterProvider } from './components/RouterProvider';
2
1
  export { Router } from './components/Router';
3
2
  export { Link } from './components/Link';
4
3
  export { Form } from './components/Form';
package/dist/index.js CHANGED
@@ -2,67 +2,6 @@ import { Suspense, createContext, lazy, useCallback, useContext, useEffect, useM
2
2
  //#region \0rolldown/runtime.js
3
3
  var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
4
4
  //#endregion
5
- //#region context/RouterProviderContext.ts
6
- var ActionsContext = createContext({});
7
- //#endregion
8
- //#region ../../node_modules/react/cjs/react-jsx-runtime.production.js
9
- /**
10
- * @license React
11
- * react-jsx-runtime.production.js
12
- *
13
- * Copyright (c) Meta Platforms, Inc. and affiliates.
14
- *
15
- * This source code is licensed under the MIT license found in the
16
- * LICENSE file in the root directory of this source tree.
17
- */
18
- var require_react_jsx_runtime_production = /* @__PURE__ */ __commonJSMin(((exports) => {
19
- var REACT_ELEMENT_TYPE = Symbol.for("react.transitional.element"), REACT_FRAGMENT_TYPE = Symbol.for("react.fragment");
20
- function jsxProd(type, config, maybeKey) {
21
- var key = null;
22
- void 0 !== maybeKey && (key = "" + maybeKey);
23
- void 0 !== config.key && (key = "" + config.key);
24
- if ("key" in config) {
25
- maybeKey = {};
26
- for (var propName in config) "key" !== propName && (maybeKey[propName] = config[propName]);
27
- } else maybeKey = config;
28
- config = maybeKey.ref;
29
- return {
30
- $$typeof: REACT_ELEMENT_TYPE,
31
- type,
32
- key,
33
- ref: void 0 !== config ? config : null,
34
- props: maybeKey
35
- };
36
- }
37
- exports.Fragment = REACT_FRAGMENT_TYPE;
38
- exports.jsx = jsxProd;
39
- exports.jsxs = jsxProd;
40
- }));
41
- //#endregion
42
- //#region provider/Provider.tsx
43
- var import_jsx_runtime = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
44
- module.exports = require_react_jsx_runtime_production();
45
- })))();
46
- var Provider = ({ children, updateLocation, prefetchLoader, invalidate }) => {
47
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ActionsContext.Provider, {
48
- value: {
49
- updateLocation,
50
- prefetchLoader,
51
- invalidate
52
- },
53
- children
54
- });
55
- };
56
- //#endregion
57
- //#region hooks/useLatest.ts
58
- var useLatest = (value) => {
59
- const ref = useRef(value);
60
- useEffect(() => {
61
- ref.current = value;
62
- }, [value]);
63
- return ref;
64
- };
65
- //#endregion
66
5
  //#region state/createState.ts
67
6
  var create = (initialState) => {
68
7
  let state = initialState;
@@ -111,60 +50,55 @@ var useRouteItemData = createState({
111
50
  var useCurrentLoaderState = createState(emptyLoaderState);
112
51
  var useScrollMap = createState({});
113
52
  var useContextState = createState({});
53
+ var useActionState = createState({});
114
54
  //#endregion
115
- //#region \0@oxc-project+runtime@0.133.0/helpers/esm/typeof.js
116
- function _typeof(o) {
117
- "@babel/helpers - typeof";
118
- return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(o) {
119
- return typeof o;
120
- } : function(o) {
121
- return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
122
- }, _typeof(o);
123
- }
124
- //#endregion
125
- //#region \0@oxc-project+runtime@0.133.0/helpers/esm/toPrimitive.js
126
- function toPrimitive(t, r) {
127
- if ("object" != _typeof(t) || !t) return t;
128
- var e = t[Symbol.toPrimitive];
129
- if (void 0 !== e) {
130
- var i = e.call(t, r || "default");
131
- if ("object" != _typeof(i)) return i;
132
- throw new TypeError("@@toPrimitive must return a primitive value.");
133
- }
134
- return ("string" === r ? String : Number)(t);
135
- }
136
- //#endregion
137
- //#region \0@oxc-project+runtime@0.133.0/helpers/esm/toPropertyKey.js
138
- function toPropertyKey(t) {
139
- var i = toPrimitive(t, "string");
140
- return "symbol" == _typeof(i) ? i : i + "";
141
- }
142
- //#endregion
143
- //#region \0@oxc-project+runtime@0.133.0/helpers/esm/defineProperty.js
144
- function _defineProperty(e, r, t) {
145
- return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
146
- value: t,
147
- enumerable: !0,
148
- configurable: !0,
149
- writable: !0
150
- }) : e[r] = t, e;
151
- }
55
+ //#region hooks/useLatest.ts
56
+ var useLatest = (value) => {
57
+ const ref = useRef(value);
58
+ useEffect(() => {
59
+ ref.current = value;
60
+ }, [value]);
61
+ return ref;
62
+ };
152
63
  //#endregion
153
- //#region config/routerConfig.ts
154
- var RouterConfig = class {
155
- constructor() {
156
- _defineProperty(this, "isAnimated", false);
157
- _defineProperty(this, "showFallbackOnAnimation", false);
158
- _defineProperty(this, "prefetch", "hover");
159
- _defineProperty(this, "hoverPrefetchDelay", 150);
160
- }
161
- configure(config) {
162
- Object.assign(this, config);
64
+ //#region ../../node_modules/react/cjs/react-jsx-runtime.production.js
65
+ /**
66
+ * @license React
67
+ * react-jsx-runtime.production.js
68
+ *
69
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
70
+ *
71
+ * This source code is licensed under the MIT license found in the
72
+ * LICENSE file in the root directory of this source tree.
73
+ */
74
+ var require_react_jsx_runtime_production = /* @__PURE__ */ __commonJSMin(((exports) => {
75
+ var REACT_ELEMENT_TYPE = Symbol.for("react.transitional.element"), REACT_FRAGMENT_TYPE = Symbol.for("react.fragment");
76
+ function jsxProd(type, config, maybeKey) {
77
+ var key = null;
78
+ void 0 !== maybeKey && (key = "" + maybeKey);
79
+ void 0 !== config.key && (key = "" + config.key);
80
+ if ("key" in config) {
81
+ maybeKey = {};
82
+ for (var propName in config) "key" !== propName && (maybeKey[propName] = config[propName]);
83
+ } else maybeKey = config;
84
+ config = maybeKey.ref;
85
+ return {
86
+ $$typeof: REACT_ELEMENT_TYPE,
87
+ type,
88
+ key,
89
+ ref: void 0 !== config ? config : null,
90
+ props: maybeKey
91
+ };
163
92
  }
164
- };
165
- var routerConfig = new RouterConfig();
93
+ exports.Fragment = REACT_FRAGMENT_TYPE;
94
+ exports.jsx = jsxProd;
95
+ exports.jsxs = jsxProd;
96
+ }));
166
97
  //#endregion
167
98
  //#region utils/createLazyComponent.tsx
99
+ var import_jsx_runtime = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
100
+ module.exports = require_react_jsx_runtime_production();
101
+ })))();
168
102
  var createLazyComponent = (importFn, fallback) => {
169
103
  const LazyComp = lazy(() => importFn().then((module) => ({ default: module.default || module })));
170
104
  return () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Suspense, {
@@ -215,9 +149,125 @@ var comparePaths = (el, pathname) => {
215
149
  return splitElementPath.every((item, index) => item === splitPathname[index + (index ? 1 : 0)]) && splitPathname.length === splitElementPath.length + paramsLength;
216
150
  };
217
151
  //#endregion
152
+ //#region hooks/useLoader.ts
153
+ var useLoader = (routes) => {
154
+ const [, setLoaderState] = useCurrentLoaderState();
155
+ const [routeItemData] = useRouteItemData();
156
+ const [context, setContext] = useContextState();
157
+ const loaderStateRef = useRef(emptyLoaderState);
158
+ const latestPathname = useLatest(routeItemData.location.pathname);
159
+ const timestampMapRef = useRef(/* @__PURE__ */ new Map());
160
+ const loaderMapRef = useRef({});
161
+ const loadingPromises = useRef(/* @__PURE__ */ new Map());
162
+ const isCacheItemFresh = useCallback(({ routeItem, pathname }) => {
163
+ if (!routeItem) return true;
164
+ const currentCacheTimestamp = timestampMapRef.current.get(pathname);
165
+ if (!currentCacheTimestamp) return false;
166
+ if (!routeItem.staleTime) return true;
167
+ return Date.now() - currentCacheTimestamp < routeItem.staleTime;
168
+ }, []);
169
+ const revalidateCache = useCallback(async ({ routeItem, pathname }) => {
170
+ if (!routeItem?.loader) return;
171
+ if (loadingPromises.current.has(pathname)) return loadingPromises.current.get(pathname);
172
+ if (isCacheItemFresh({
173
+ routeItem,
174
+ pathname
175
+ })) {
176
+ loaderStateRef.current = loaderMapRef.current[pathname];
177
+ return;
178
+ }
179
+ const promise = (async () => {
180
+ if (!routeItem?.loader) return;
181
+ try {
182
+ const params = getParamsObject({
183
+ params: routeItem.params,
184
+ pathname
185
+ });
186
+ const result = await routeItem?.loader({
187
+ params,
188
+ context,
189
+ setContext
190
+ });
191
+ timestampMapRef.current.set(pathname, Date.now());
192
+ loaderStateRef.current = {
193
+ ...loaderStateRef?.current,
194
+ data: result,
195
+ loaderError: null
196
+ };
197
+ loaderMapRef.current[pathname] = loaderStateRef.current;
198
+ } catch (error) {
199
+ loaderStateRef.current = {
200
+ ...loaderStateRef?.current,
201
+ data: null,
202
+ loaderError: error
203
+ };
204
+ } finally {
205
+ loadingPromises.current.delete(pathname);
206
+ }
207
+ })();
208
+ loadingPromises.current.set(pathname, promise);
209
+ return promise;
210
+ }, [
211
+ context,
212
+ isCacheItemFresh,
213
+ setContext
214
+ ]);
215
+ return {
216
+ prefetchLoader: useCallback(async (pathname) => {
217
+ const item = routes.find((el) => comparePaths(el, pathname));
218
+ if (item) await revalidateCache({
219
+ routeItem: item,
220
+ pathname
221
+ });
222
+ }, [revalidateCache, routes]),
223
+ revalidateCache,
224
+ isCacheItemFresh,
225
+ loaderStateRef,
226
+ invalidate: useCallback(async (pathname = latestPathname.current) => {
227
+ if (typeof pathname !== "string") return;
228
+ const routeItem = routes.find((el) => comparePaths(el, pathname));
229
+ const resultParams = getParamsObject({
230
+ params: routeItem?.params,
231
+ pathname
232
+ });
233
+ timestampMapRef.current.delete(pathname);
234
+ try {
235
+ if (routeItem?.beforeLoad) await routeItem.beforeLoad({
236
+ context,
237
+ redirect: () => Promise.resolve(),
238
+ params: resultParams,
239
+ setContext
240
+ });
241
+ loaderStateRef.current = {
242
+ ...loaderStateRef.current,
243
+ beforeLoadError: null
244
+ };
245
+ } catch (error) {
246
+ loaderStateRef.current = {
247
+ ...loaderStateRef.current,
248
+ beforeLoadError: error
249
+ };
250
+ }
251
+ await revalidateCache({
252
+ routeItem,
253
+ pathname
254
+ });
255
+ if (pathname === latestPathname.current) setLoaderState(loaderStateRef.current);
256
+ }, [
257
+ context,
258
+ loaderStateRef,
259
+ revalidateCache,
260
+ latestPathname,
261
+ routes,
262
+ setContext,
263
+ setLoaderState
264
+ ])
265
+ };
266
+ };
267
+ //#endregion
218
268
  //#region hooks/useNavigation.ts
219
269
  var ALL_LOCATIONS = "*";
220
- var useNavigation = ({ routes, revalidateCache, isCacheItemFresh, loaderStateRef }) => {
270
+ var useNavigation = ({ routes, revalidateCache, isCacheItemFresh, loaderStateRef, isAnimated, showFallbackOnAnimation: showFallback }) => {
221
271
  const [, setIsLoading] = useIsLoading();
222
272
  const [, setScrollMap] = useScrollMap();
223
273
  const [, setRouteItemData] = useRouteItemData();
@@ -225,7 +275,6 @@ var useNavigation = ({ routes, revalidateCache, isCacheItemFresh, loaderStateRef
225
275
  const [, setCurrentLoaderFallback] = useLoaderFallback();
226
276
  const [context, setContext] = useContextState();
227
277
  const [blockedRoute, setBlockedRoute] = useBlockedRoute();
228
- const { isAnimated, showFallbackOnAnimation: showFallback } = routerConfig;
229
278
  const prevPathname = useRef("");
230
279
  const navigationSeq = useRef(0);
231
280
  const navigation = useCallback((nextLocation, routeItem) => {
@@ -248,7 +297,7 @@ var useNavigation = ({ routes, revalidateCache, isCacheItemFresh, loaderStateRef
248
297
  setRouteItemData
249
298
  ]);
250
299
  const transitionedNavigation = useCallback((nextLocation, routeItem) => {
251
- if (!isAnimated) {
300
+ if (!isAnimated || !prevPathname.current) {
252
301
  navigation(nextLocation, routeItem);
253
302
  return;
254
303
  }
@@ -357,136 +406,21 @@ var useNavigation = ({ routes, revalidateCache, isCacheItemFresh, loaderStateRef
357
406
  return updateLocation;
358
407
  };
359
408
  //#endregion
360
- //#region hooks/useLoader.ts
361
- var useLoader = (routes) => {
362
- const [, setLoaderState] = useCurrentLoaderState();
363
- const [routeItemData] = useRouteItemData();
364
- const [context, setContext] = useContextState();
365
- const loaderStateRef = useRef(emptyLoaderState);
366
- const latestPathname = useLatest(routeItemData.location.pathname);
367
- const timestampMapRef = useRef(/* @__PURE__ */ new Map());
368
- const loaderMapRef = useRef({});
369
- const loadingPromises = useRef(/* @__PURE__ */ new Map());
370
- const isCacheItemFresh = useCallback(({ routeItem, pathname }) => {
371
- if (!routeItem) return true;
372
- const currentCacheTimestamp = timestampMapRef.current.get(pathname);
373
- if (!currentCacheTimestamp) return false;
374
- if (!routeItem.staleTime) return true;
375
- return Date.now() - currentCacheTimestamp < routeItem.staleTime;
376
- }, []);
377
- const revalidateCache = useCallback(async ({ routeItem, pathname }) => {
378
- if (!routeItem?.loader) return;
379
- if (loadingPromises.current.has(pathname)) return loadingPromises.current.get(pathname);
380
- if (isCacheItemFresh({
381
- routeItem,
382
- pathname
383
- })) {
384
- loaderStateRef.current = loaderMapRef.current[pathname];
385
- return;
386
- }
387
- const promise = (async () => {
388
- if (!routeItem?.loader) return;
389
- try {
390
- const params = getParamsObject({
391
- params: routeItem.params,
392
- pathname
393
- });
394
- const result = await routeItem?.loader({
395
- params,
396
- context,
397
- setContext
398
- });
399
- timestampMapRef.current.set(pathname, Date.now());
400
- loaderStateRef.current = {
401
- ...loaderStateRef?.current,
402
- data: result,
403
- loaderError: null
404
- };
405
- loaderMapRef.current[pathname] = loaderStateRef.current;
406
- } catch (error) {
407
- loaderStateRef.current = {
408
- ...loaderStateRef?.current,
409
- data: null,
410
- loaderError: error
411
- };
412
- } finally {
413
- loadingPromises.current.delete(pathname);
414
- }
415
- })();
416
- loadingPromises.current.set(pathname, promise);
417
- return promise;
409
+ //#region hooks/useSetRuntime.ts
410
+ var useSetRouterRuntime = ({ updateLocation, prefetchLoader, invalidate }) => {
411
+ const [, setCallbackState] = useActionState();
412
+ useEffect(() => {
413
+ setCallbackState({
414
+ updateLocation,
415
+ prefetchLoader,
416
+ invalidate
417
+ });
418
418
  }, [
419
- context,
420
- isCacheItemFresh,
421
- setContext
422
- ]);
423
- return {
424
- prefetchLoader: useCallback(async (pathname) => {
425
- const item = routes.find((el) => comparePaths(el, pathname));
426
- if (item) await revalidateCache({
427
- routeItem: item,
428
- pathname
429
- });
430
- }, [revalidateCache, routes]),
431
- revalidateCache,
432
- isCacheItemFresh,
433
- loaderStateRef,
434
- invalidate: useCallback(async (pathname = latestPathname.current) => {
435
- if (typeof pathname !== "string") return;
436
- const routeItem = routes.find((el) => comparePaths(el, pathname));
437
- const resultParams = getParamsObject({
438
- params: routeItem?.params,
439
- pathname
440
- });
441
- timestampMapRef.current.delete(pathname);
442
- try {
443
- if (routeItem?.beforeLoad) await routeItem.beforeLoad({
444
- context,
445
- redirect: () => Promise.resolve(),
446
- params: resultParams,
447
- setContext
448
- });
449
- loaderStateRef.current = {
450
- ...loaderStateRef.current,
451
- beforeLoadError: null
452
- };
453
- } catch (error) {
454
- loaderStateRef.current = {
455
- ...loaderStateRef.current,
456
- beforeLoadError: error
457
- };
458
- }
459
- await revalidateCache({
460
- routeItem,
461
- pathname
462
- });
463
- if (pathname === latestPathname.current) setLoaderState(loaderStateRef.current);
464
- }, [
465
- context,
466
- loaderStateRef,
467
- revalidateCache,
468
- latestPathname,
469
- routes,
470
- setContext,
471
- setLoaderState
472
- ])
473
- };
474
- };
475
- //#endregion
476
- //#region components/RouterProvider.tsx
477
- var RouterProvider = ({ children, routes }) => {
478
- const { prefetchLoader, revalidateCache, isCacheItemFresh, loaderStateRef, invalidate } = useLoader(routes);
479
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Provider, {
480
- updateLocation: useNavigation({
481
- routes,
482
- revalidateCache,
483
- isCacheItemFresh,
484
- loaderStateRef
485
- }),
486
419
  invalidate,
487
420
  prefetchLoader,
488
- children
489
- });
421
+ setCallbackState,
422
+ updateLocation
423
+ ]);
490
424
  };
491
425
  //#endregion
492
426
  //#region hooks/useApplyCustomAnimation.ts
@@ -549,6 +483,56 @@ var usePreserveScroll = (preserveScroll) => {
549
483
  ]);
550
484
  };
551
485
  //#endregion
486
+ //#region \0@oxc-project+runtime@0.133.0/helpers/esm/typeof.js
487
+ function _typeof(o) {
488
+ "@babel/helpers - typeof";
489
+ return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(o) {
490
+ return typeof o;
491
+ } : function(o) {
492
+ return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
493
+ }, _typeof(o);
494
+ }
495
+ //#endregion
496
+ //#region \0@oxc-project+runtime@0.133.0/helpers/esm/toPrimitive.js
497
+ function toPrimitive(t, r) {
498
+ if ("object" != _typeof(t) || !t) return t;
499
+ var e = t[Symbol.toPrimitive];
500
+ if (void 0 !== e) {
501
+ var i = e.call(t, r || "default");
502
+ if ("object" != _typeof(i)) return i;
503
+ throw new TypeError("@@toPrimitive must return a primitive value.");
504
+ }
505
+ return ("string" === r ? String : Number)(t);
506
+ }
507
+ //#endregion
508
+ //#region \0@oxc-project+runtime@0.133.0/helpers/esm/toPropertyKey.js
509
+ function toPropertyKey(t) {
510
+ var i = toPrimitive(t, "string");
511
+ return "symbol" == _typeof(i) ? i : i + "";
512
+ }
513
+ //#endregion
514
+ //#region \0@oxc-project+runtime@0.133.0/helpers/esm/defineProperty.js
515
+ function _defineProperty(e, r, t) {
516
+ return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
517
+ value: t,
518
+ enumerable: !0,
519
+ configurable: !0,
520
+ writable: !0
521
+ }) : e[r] = t, e;
522
+ }
523
+ //#endregion
524
+ //#region config/routerConfig.ts
525
+ var RouterConfig = class {
526
+ constructor() {
527
+ _defineProperty(this, "prefetch", "hover");
528
+ _defineProperty(this, "hoverPrefetchDelay", 150);
529
+ }
530
+ configure(config) {
531
+ Object.assign(this, config);
532
+ }
533
+ };
534
+ var routerConfig = new RouterConfig();
535
+ //#endregion
552
536
  //#region hooks/useSetRouterConfig.ts
553
537
  var useSetRouterConfig = (routerProps) => {
554
538
  useEffect(() => routerConfig.configure(routerProps), [routerProps]);
@@ -573,41 +557,61 @@ var renderElement = (Component) => {
573
557
  //#endregion
574
558
  //#region components/Router.tsx
575
559
  var EmptyBoundary = ({ children }) => children;
576
- var Router = ({ isAnimated, animationDuration, spinner = true, preserveScroll = true, showFallbackOnAnimation = false, prefetch = "hover", hoverPrefetchDelay = 150, errorBoundary: ErrorBoundary = EmptyBoundary, context: initialContext }) => {
560
+ var Router = ({ routes, animationDuration, isAnimated = false, spinner = true, preserveScroll = true, showFallbackOnAnimation = false, prefetch = "hover", hoverPrefetchDelay = 150, errorBoundary: ErrorBoundary = EmptyBoundary, context: initialContext, defaultLoaderFallback, defaultErrorElement }) => {
577
561
  const [isLoading] = useIsLoading();
578
562
  const [currentLoaderFallback] = useLoaderFallback();
579
563
  const [routeItemData] = useRouteItemData();
580
564
  const [loaderState] = useCurrentLoaderState();
581
- usePreserveScroll(preserveScroll);
582
- useApplyCustomAnimation(animationDuration);
565
+ const { prefetchLoader, revalidateCache, isCacheItemFresh, loaderStateRef, invalidate } = useLoader(routes);
566
+ useSetRouterRuntime({
567
+ updateLocation: useNavigation({
568
+ routes,
569
+ revalidateCache,
570
+ isCacheItemFresh,
571
+ loaderStateRef,
572
+ isAnimated,
573
+ showFallbackOnAnimation
574
+ }),
575
+ prefetchLoader,
576
+ invalidate
577
+ });
583
578
  useSetRouterConfig({
584
579
  isAnimated,
585
580
  showFallbackOnAnimation,
586
581
  prefetch,
587
582
  hoverPrefetchDelay
588
583
  });
584
+ useApplyCustomAnimation(animationDuration);
589
585
  useSetInitialContext(initialContext);
586
+ usePreserveScroll(preserveScroll);
590
587
  const showErrorElement = !isLoading && Boolean(loaderState.loaderError || loaderState.beforeLoadError);
591
588
  const showSpinner = spinner && isAnimated && isLoading;
592
589
  const loadingContent = !showErrorElement && isLoading;
593
590
  const { routeItem, location } = routeItemData;
594
- if ((showFallbackOnAnimation || !isAnimated) && loadingContent) return renderElement(currentLoaderFallback);
591
+ if ((showFallbackOnAnimation || !isAnimated) && loadingContent) return renderElement(currentLoaderFallback || defaultLoaderFallback);
595
592
  if (!showFallbackOnAnimation && isAnimated && loadingContent) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {});
596
593
  if (!routeItem) return null;
597
- if (showErrorElement) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [renderElement(routeItem.errorElement), showSpinner && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {})] });
594
+ if (showErrorElement) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [renderElement(routeItem.errorElement || defaultErrorElement), showSpinner && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {})] });
598
595
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
599
596
  style: { viewTransitionName: "page" },
600
597
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ErrorBoundary, { children: renderElement(routeItem.element) }, location.pathname), showSpinner && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {})]
601
598
  });
602
599
  };
603
600
  //#endregion
604
- //#region hooks/useServiceContext.ts
605
- var useServiceState = (reactContext) => {
606
- const context = useContext(reactContext);
607
- if (!Object.keys(context).length) throw new Error("hooks and Router component must be used within RouterProvider");
608
- return context;
601
+ //#region hooks/useRuntime.ts
602
+ var useRuntime = () => {
603
+ const [callbackState] = useActionState();
604
+ const { updateLocation, prefetchLoader, invalidate } = callbackState;
605
+ return useMemo(() => ({
606
+ updateLocation,
607
+ prefetchLoader,
608
+ invalidate
609
+ }), [
610
+ updateLocation,
611
+ prefetchLoader,
612
+ invalidate
613
+ ]);
609
614
  };
610
- var useRouterActions = () => useServiceState(ActionsContext);
611
615
  //#endregion
612
616
  //#region hooks/useLocation.ts
613
617
  var useLocation = () => {
@@ -618,10 +622,11 @@ var useLocation = () => {
618
622
  //#region hooks/useNavigate.ts
619
623
  var useNavigate = () => {
620
624
  const [blockedRoute, setBlockedRoute] = useBlockedRoute();
621
- const { updateLocation } = useRouterActions();
625
+ const { updateLocation } = useRuntime();
622
626
  const locationRef = useLatest(useLocation());
623
627
  const blockedRouteRef = useLatest(blockedRoute);
624
628
  return useCallback(async (arg) => {
629
+ if (!updateLocation) throw new Error("Router has not been initialized. Did you forget to render <Router />?");
625
630
  if (arg !== -1 && blockedRouteRef.current.from) {
626
631
  const to = typeof arg === "object" ? arg.pathname : arg;
627
632
  setBlockedRoute((prevState) => ({
@@ -647,7 +652,7 @@ var Link = ({ children, to, prefetch: prefetchLink, hoverPrefetchDelay }) => {
647
652
  const { prefetch: configPrefetch, hoverPrefetchDelay: configPrefetchDelay } = routerConfig;
648
653
  const prefetch = prefetchLink || configPrefetch;
649
654
  const prefetchDelay = hoverPrefetchDelay ?? configPrefetchDelay;
650
- const { prefetchLoader } = useRouterActions();
655
+ const { prefetchLoader } = useRuntime();
651
656
  const navigate = useNavigate();
652
657
  const timeout = useRef(0);
653
658
  const ref = useRef(null);
@@ -715,7 +720,7 @@ var FormProvider = ({ children, isSubmitting }) => /* @__PURE__ */ (0, import_js
715
720
  //#endregion
716
721
  //#region hooks/useInvalidate.ts
717
722
  var useInvalidate = () => {
718
- const { invalidate } = useRouterActions();
723
+ const { invalidate } = useRuntime();
719
724
  return invalidate;
720
725
  };
721
726
  //#endregion
@@ -788,7 +793,7 @@ var useLoaderState = () => {
788
793
  //#region hooks/useBlocker.ts
789
794
  var useBlocker = (blockerFn) => {
790
795
  const [blockedRoute, setBlockedRoute] = useBlockedRoute();
791
- const { updateLocation } = useRouterActions();
796
+ const { updateLocation } = useRuntime();
792
797
  const [routeItemData] = useRouteItemData();
793
798
  const { location: { pathname } } = routeItemData;
794
799
  const updateBlockedRoute = useCallback(({ type, payload = "" }) => setBlockedRoute((prevState) => {
@@ -1009,4 +1014,4 @@ var adapter = {
1009
1014
  })
1010
1015
  };
1011
1016
  //#endregion
1012
- export { Form, Link, Router, RouterProvider, adapter, createRouter, useAction, useBlocker, useFormContext, useInvalidate, useLoaderState, useLocation, useNavigate, useParams, useQueryParam, useRouterContext, useSearchParams };
1017
+ export { Form, Link, Router, adapter, createRouter, useAction, useBlocker, useFormContext, useInvalidate, useLoaderState, useLocation, useNavigate, useParams, useQueryParam, useRouterContext, useSearchParams };
@@ -1,4 +1,9 @@
1
- import { LoaderState, RouteItemData } from '../types/global';
1
+ import { LoaderState, Location, RouteItemData } from '../types/global';
2
+ type Runtime = {
3
+ updateLocation(route: Location): Promise<void>;
4
+ prefetchLoader(arg: string): Promise<void>;
5
+ invalidate(path?: string): Promise<void>;
6
+ };
2
7
  export declare const useIsLoading: () => readonly [boolean, (action: boolean | ((prevState: boolean) => boolean)) => void];
3
8
  export declare const useBlockedRoute: () => readonly [{
4
9
  from: string;
@@ -18,3 +23,5 @@ export declare const useRouteItemData: () => readonly [RouteItemData, (action: R
18
23
  export declare const useCurrentLoaderState: () => readonly [LoaderState, (action: LoaderState | ((prevState: LoaderState) => LoaderState)) => void];
19
24
  export declare const useScrollMap: () => readonly [Record<string, number>, (action: Record<string, number> | ((prevState: Record<string, number>) => Record<string, number>)) => void];
20
25
  export declare const useContextState: () => readonly [Record<string, unknown>, (action: Record<string, unknown> | ((prevState: Record<string, unknown>) => Record<string, unknown>)) => void];
26
+ export declare const useActionState: () => readonly [Runtime, (action: Runtime | ((prevState: Runtime) => Runtime)) => void];
27
+ export {};
@@ -66,10 +66,13 @@ export type RouteItemData = {
66
66
  routeItem: RouteItem | undefined;
67
67
  };
68
68
  export type RouterProps = {
69
+ routes: RouteItem[];
69
70
  isAnimated?: boolean;
70
71
  animationDuration?: number;
71
72
  spinner?: boolean;
72
73
  preserveScroll?: boolean;
74
+ defaultLoaderFallback?: Element;
75
+ defaultErrorElement?: Element;
73
76
  showFallbackOnAnimation?: boolean;
74
77
  prefetch?: 'hover' | 'render' | 'viewport' | 'none';
75
78
  hoverPrefetchDelay?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clear-react-router",
3
- "version": "1.7.1",
3
+ "version": "1.7.3",
4
4
  "description": "A lightweight, type-safe routing library for React applications",
5
5
  "author": "Andrew Bubnov",
6
6
  "scripts": {
@@ -1,8 +0,0 @@
1
- import { ReactNode } from 'react';
2
- import { RouteItem } from '../types/global';
3
- type RouteProviderProps = {
4
- children: ReactNode;
5
- routes: RouteItem[];
6
- };
7
- export declare const RouterProvider: ({ children, routes }: RouteProviderProps) => import("react/jsx-runtime").JSX.Element;
8
- export {};
@@ -1,7 +0,0 @@
1
- import { Location } from '../types/global';
2
- export type ActionsContextValue = {
3
- updateLocation(route: Location): Promise<void>;
4
- prefetchLoader(arg: string): Promise<void>;
5
- invalidate(path?: string): Promise<void>;
6
- };
7
- export declare const ActionsContext: import("react").Context<ActionsContextValue>;
@@ -1 +0,0 @@
1
- export declare const useRouterActions: () => import("../context/RouterProviderContext").ActionsContextValue;
@@ -1,7 +0,0 @@
1
- import { type ReactNode } from 'react';
2
- import { type ActionsContextValue } from '../context/RouterProviderContext';
3
- type ProviderProps = ActionsContextValue & {
4
- children: ReactNode;
5
- };
6
- export declare const Provider: ({ children, updateLocation, prefetchLoader, invalidate }: ProviderProps) => import("react/jsx-runtime").JSX.Element;
7
- export {};