clear-react-router 1.9.7 → 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.
package/README.md CHANGED
@@ -572,6 +572,8 @@ const UserProfile = () => {
572
572
  staleTime: 60000, // 1 minute — cache is fresh for 60 seconds
573
573
  }
574
574
  ```
575
+ - Stale cache entries are cleaned up incrementally on every navigation, keeping the loader cache from growing unbounded over long sessions.
576
+
575
577
 
576
578
  ### `useInvalidate()`
577
579
 
@@ -773,11 +775,13 @@ function ProductFilter() {
773
775
 
774
776
  ## Lazy Loading
775
777
 
776
- Clear Router supports code-splitting out of the box. Simply pass a function that returns a dynamic import:
778
+ Clear Router supports code-splitting out of the box. Simply wrap dynamic import into a library's `lazy` function:
777
779
  ```tsx
780
+ import { lazy } from 'clear-react-router';
781
+
778
782
  {
779
783
  path: '/heavy-page',
780
- element: () => import('./pages/HeavyComponent'),
784
+ element: lazy(() => import('./pages/HeavyComponent')),
781
785
  fallback: () => <div>Loading...</div>,
782
786
  }
783
787
  ```
package/dist/index.d.ts CHANGED
@@ -11,5 +11,6 @@ export { useAction } from './hooks/useAction';
11
11
  export { useRouterContext } from './hooks/useRouterContext';
12
12
  export { useSearchParams } from './hooks/useSearchParams';
13
13
  export { useFormContext } from './hooks/useFormContext';
14
+ export { lazy } from './utils/lazy';
14
15
  export { createRouter } from './utils/utils';
15
16
  export type { RouteItem, BlockerState, Location, RouterProps, ElementProps } from './types';
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Suspense, createContext, lazy, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
1
+ import { Suspense, createContext, lazy as lazy$1, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
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
@@ -113,13 +113,12 @@ var createCommitNavigation = (navigationExecutor, routeItemDataState) => (nextLo
113
113
  };
114
114
  //#endregion
115
115
  //#region utils/isCacheItemFresh.ts
116
- var createIsCacheItemFresh = (loaderMap) => ({ routeItem, pathname }) => {
117
- if (!routeItem) return true;
118
- const item = loaderMap.get(pathname);
116
+ var createIsCacheItemFresh = (loaderMap) => (pathname, search) => {
117
+ const item = loaderMap.get(`${pathname}${search}`);
119
118
  if (item === void 0) return false;
120
- const staleTime = routeItem.staleTime ?? routerConfig.defaultStaleTime;
121
- if (staleTime === void 0) return true;
122
- return Date.now() - item.timestamp <= staleTime;
119
+ const resolvedStaleTime = item.staleTime ?? routerConfig.defaultStaleTime;
120
+ if (resolvedStaleTime === void 0) return true;
121
+ return Date.now() - item.timestamp <= resolvedStaleTime;
123
122
  };
124
123
  //#endregion
125
124
  //#region ../../node_modules/react/cjs/react-jsx-runtime.production.js
@@ -162,7 +161,7 @@ var import_jsx_runtime = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
162
161
  })))();
163
162
  var createLazyComponent = (importFn, fallback) => {
164
163
  const load = () => importFn().then((module) => ({ default: module.default || module }));
165
- const LazyComp = lazy(load);
164
+ const LazyComp = lazy$1(load);
166
165
  const Component = () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Suspense, {
167
166
  fallback: typeof fallback === "function" ? fallback() : fallback || null,
168
167
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LazyComp, {})
@@ -173,12 +172,15 @@ var createLazyComponent = (importFn, fallback) => {
173
172
  };
174
173
  };
175
174
  //#endregion
175
+ //#region types.ts
176
+ var LAZY_MARKER = Symbol("clear-router-lazy");
177
+ //#endregion
176
178
  //#region utils/utils.ts
177
- var isLazy = (el) => typeof el.element === "function" && el.element.toString().includes("import(");
179
+ var isLazy = (el) => typeof el.element === "object" && el.element !== null && LAZY_MARKER in el.element;
178
180
  var parseClientRouteItem = (el, parentPattern = "") => {
179
181
  const pattern = `${parentPattern}/${el.path}`.replace(/\/+/g, "/");
180
- const preloadElement = isLazy(el) ? createLazyComponent(el.element, el.fallback).preloadElement : void 0;
181
- const resolvedElement = isLazy(el) ? createLazyComponent(el.element, el.fallback).Component : el.element;
182
+ const preloadElement = isLazy(el) ? createLazyComponent(el.element.importFn, el.fallback).preloadElement : void 0;
183
+ const resolvedElement = isLazy(el) ? createLazyComponent(el.element.importFn, el.fallback).Component : el.element;
182
184
  return [{
183
185
  ...el,
184
186
  pattern,
@@ -278,17 +280,14 @@ var createNavigate = (routerState, revalidateCache) => {
278
280
  const currentLoaderState = loaderMap.get(location.pathname)?.state;
279
281
  if (currentLoaderState) loaderStateRef.set(currentLoaderState);
280
282
  } else {
281
- const pendingShouldExist = routeItem?.loader && !isCacheItemFresh({
282
- routeItem,
283
- pathname: location.pathname
284
- });
283
+ const pendingShouldExist = routeItem?.loader && !isCacheItemFresh(location.pathname, location.search);
285
284
  pendingState.setState(pendingShouldExist ? {
286
285
  routeItem,
287
286
  location
288
287
  } : void 0);
289
288
  }
290
289
  };
291
- const afterEachLoad = (routeItem) => {
290
+ const afterEachLoad = (routeItem, location) => {
292
291
  if (!routeItem?.pollingInterval) return;
293
292
  interval = window.setInterval(() => revalidateCache({
294
293
  routeItem,
@@ -303,7 +302,7 @@ var createNavigate = (routerState, revalidateCache) => {
303
302
  pathname: location.pathname,
304
303
  search: location.search
305
304
  });
306
- afterEachLoad(routeItem);
305
+ afterEachLoad(routeItem, location);
307
306
  };
308
307
  const afterLoad = async (routeItem, params) => {
309
308
  const { defaultAfterLoad } = routerConfig;
@@ -420,15 +419,20 @@ var getRetry = (routeItem) => {
420
419
  var sleep = async (ms) => new Promise((resolve) => setTimeout(resolve, ms));
421
420
  var createRevalidateCache = (routerState) => {
422
421
  const { loaderStateRef, contextState, loaderMap } = routerState;
422
+ const removeFirstStaleItem = () => {
423
+ const deletedItem = [...loaderMap.entries()].find(([, item]) => {
424
+ const staleTime = item.staleTime ?? routerConfig.defaultStaleTime;
425
+ return staleTime && staleTime + item.timestamp < Date.now();
426
+ });
427
+ if (deletedItem) loaderMap.delete(deletedItem[0]);
428
+ };
423
429
  const revalidateCache = async ({ routeItem, pathname, search = "" }, retried = 0) => {
424
430
  if (!routeItem?.loader) return;
425
431
  const isCacheItemFresh = createIsCacheItemFresh(loaderMap);
426
- if (loadingPromises.has(pathname)) return loadingPromises.get(pathname);
427
- if (isCacheItemFresh({
428
- routeItem,
429
- pathname
430
- })) {
431
- const item = loaderMap.get(pathname);
432
+ removeFirstStaleItem();
433
+ if (loadingPromises.has(pathname)) return loadingPromises.get(`${pathname}${search}`);
434
+ if (isCacheItemFresh(pathname, search)) {
435
+ const item = loaderMap.get(`${pathname}${search}`);
432
436
  if (item?.state) loaderStateRef.set(item.state);
433
437
  return;
434
438
  }
@@ -452,7 +456,8 @@ var createRevalidateCache = (routerState) => {
452
456
  }));
453
457
  loaderMap.set(`${pathname}${search}`, {
454
458
  state: loaderStateRef.value,
455
- timestamp: Date.now()
459
+ timestamp: Date.now(),
460
+ staleTime: routeItem.staleTime
456
461
  });
457
462
  return {
458
463
  data: result,
@@ -465,7 +470,8 @@ var createRevalidateCache = (routerState) => {
465
470
  if (retry.delay) await sleep(retry.delay);
466
471
  await revalidateCache({
467
472
  routeItem,
468
- pathname
473
+ pathname,
474
+ search
469
475
  }, retried + 1);
470
476
  return {
471
477
  data: null,
@@ -709,7 +715,8 @@ var renderElement = (Component) => {
709
715
  //#endregion
710
716
  //#region components/Router.tsx
711
717
  var EmptyBoundary = ({ children }) => children;
712
- var Router = ({ routes, defaultBeforeLoad, defaultAfterLoad, animationDuration, defaultLoaderFallback, defaultErrorElement, defaultRetry, defaultStaleTime, context: initialContext, isAnimated = false, spinner = true, defaultPreserveScroll = true, showFallbackOnAnimation = false, defaultPrefetch = isMobile() ? "viewport" : "hover", defaultHoverPrefetchDelay = 150, errorBoundary: ErrorBoundary = EmptyBoundary }) => {
718
+ var IS_MOBILE = isMobile();
719
+ var Router = ({ routes, defaultBeforeLoad, defaultAfterLoad, animationDuration, defaultLoaderFallback, defaultErrorElement, defaultRetry, defaultStaleTime, context: initialContext, isAnimated = false, spinner = true, defaultPreserveScroll = true, showFallbackOnAnimation = false, defaultPrefetch = IS_MOBILE ? "viewport" : "hover", defaultHoverPrefetchDelay = 150, errorBoundary: ErrorBoundary = EmptyBoundary }) => {
713
720
  const { useRouteItemData, usePendingState } = router.hooks;
714
721
  const [routeItemData] = useRouteItemData();
715
722
  const [pendingState] = usePendingState();
@@ -1006,4 +1013,10 @@ var useSearchParams = () => {
1006
1013
  //#region hooks/useFormContext.ts
1007
1014
  var useFormContext = () => useContext(FormContext);
1008
1015
  //#endregion
1009
- export { Form, Link, Router, createRouter, useAction, useBlocker, useFormContext, useInvalidate, useLoaderState, useLocation, useNavigate, useParams, useRouterContext, useSearchParams };
1016
+ //#region utils/lazy.ts
1017
+ var lazy = (importFn) => ({
1018
+ [LAZY_MARKER]: true,
1019
+ importFn
1020
+ });
1021
+ //#endregion
1022
+ export { Form, Link, Router, createRouter, lazy, useAction, useBlocker, useFormContext, useInvalidate, useLoaderState, useLocation, useNavigate, useParams, useRouterContext, useSearchParams };
package/dist/types.d.ts CHANGED
@@ -1,9 +1,13 @@
1
1
  import { ComponentType, type CSSProperties, Dispatch, MouseEvent, ReactElement, ReactNode, Ref, SetStateAction } from 'react';
2
2
  import { Store, useGlobalState } from './create';
3
3
  import { Cell } from './cell';
4
- export type LazyComponent = () => Promise<{
5
- default: ComponentType<unknown>;
6
- }>;
4
+ export declare const LAZY_MARKER: unique symbol;
5
+ export type LazyComponent = {
6
+ readonly [LAZY_MARKER]: true;
7
+ importFn: () => Promise<{
8
+ default: ComponentType<unknown>;
9
+ }>;
10
+ };
7
11
  export type RenderElement = (() => ReactElement) | ReactElement;
8
12
  export type BeforeLoad = (arg: {
9
13
  context: Record<string, unknown>;
@@ -98,6 +102,7 @@ export type RouterProps = {
98
102
  export type LoaderStateItem = {
99
103
  state: LoaderState;
100
104
  timestamp: number;
105
+ staleTime: number | undefined;
101
106
  };
102
107
  export type RouterState = {
103
108
  routeItemDataState: Store<RouteItemData>;
@@ -1,5 +1,2 @@
1
- import { LoaderStateItem, RouteItem } from '../types';
2
- export declare const createIsCacheItemFresh: (loaderMap: Map<string, LoaderStateItem>) => ({ routeItem, pathname }: {
3
- routeItem?: RouteItem;
4
- pathname: string;
5
- }) => boolean;
1
+ import { LoaderStateItem } from '../types';
2
+ export declare const createIsCacheItemFresh: (loaderMap: Map<string, LoaderStateItem>) => (pathname: string, search?: string) => boolean;
@@ -0,0 +1,5 @@
1
+ import { ComponentType } from 'react';
2
+ import { LazyComponent } from '../types';
3
+ export declare const lazy: (importFn: () => Promise<{
4
+ default: ComponentType<unknown>;
5
+ }>) => LazyComponent;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clear-react-router",
3
- "version": "1.9.7",
3
+ "version": "2.0.0",
4
4
  "description": "A lightweight, type-safe routing library for React applications",
5
5
  "author": "Andrew Bubnov",
6
6
  "scripts": {