clear-react-router 1.4.5 → 1.4.7

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
@@ -45,7 +45,6 @@ The root component that provides routing context to the application. Place stati
45
45
  |------|------|---------|-------------|
46
46
  | `routeList` | `RouteItem[]` | required | Array of route configurations |
47
47
  | `context` | `object` | `{}` | Initial context (user, theme, etc.) |
48
- | `preserveScroll` | `boolean` | `true` | Save and restore scroll position when navigating between pages |
49
48
  | `children` | `ReactNode` | required | App content (must include `<Router />`) |
50
49
 
51
50
  ```
@@ -71,6 +70,8 @@ Renders the current route's component. Must be placed inside `<RouterProvider>`.
71
70
  | `isAnimated` | `boolean \| undefined` | `false` | Enable smooth page fade transitions |
72
71
  | `animationDuration` | `number` | `optional` | Animation duration in milliseconds (browser default is used if not set) |
73
72
  | `spinner` | `boolean \| undefined` | `true` | Show a small spinner in the corner while loading data (only when `isAnimated` is enabled) |
73
+ | `preserveScroll` | `boolean \| undefined` | `true` | Save and restore scroll position when navigating between pages |
74
+ | `showFallbackIfAnimated` | `boolean \| undefined` | `false` | Show `loaderFallback` even when `isAnimated` is `true` (instead of spinner) |
74
75
 
75
76
  ```
76
77
  <RouterProvider routeList={routes} isAnimated>
@@ -3,6 +3,7 @@ type RouterProps = {
3
3
  animationDuration?: number;
4
4
  spinner?: boolean;
5
5
  preserveScroll?: boolean;
6
+ showFallbackIfAnimated?: boolean;
6
7
  };
7
- export declare const Router: ({ isAnimated, animationDuration, spinner, preserveScroll }: RouterProps) => import("react/jsx-runtime").JSX.Element | null;
8
+ export declare const Router: ({ isAnimated, animationDuration, spinner, preserveScroll, showFallbackIfAnimated, }: RouterProps) => import("react/jsx-runtime").JSX.Element | null;
8
9
  export {};
@@ -1,26 +1,21 @@
1
- import { Dispatch, SetStateAction } from 'react';
2
- import { BlockerState, LoaderState, Location, RouteItem, UpdateBlockedRouteProps } from '../types/global';
3
- export type PropsContextValue = {
4
- routeList: RouteItem[];
5
- };
1
+ import { BlockerState, Location, RouteItem, RouteItemData, UpdateBlockedRouteProps } from '../types/global';
6
2
  export type NavigationContextValue = {
7
- location: Location;
8
3
  blockerState: BlockerState;
9
4
  isLoading: boolean;
10
- nextItem: RouteItem | undefined;
5
+ routeItemData: RouteItemData;
6
+ currentLoaderFallback: RouteItem['loaderFallback'];
11
7
  };
12
8
  export type ActionsContextValue = {
13
- setLocation: Dispatch<SetStateAction<Location>>;
9
+ setSearch(arg: string): void;
14
10
  updateLocation(route: Location): Promise<void>;
15
11
  updateBlockedRoute(arg: UpdateBlockedRouteProps): void;
16
12
  prefetchLoader(arg: string): Promise<void>;
17
13
  setContext(arg: object): void;
14
+ restoreScroll(): void;
18
15
  };
19
16
  export type DataContextValue = {
20
- loaderState: LoaderState;
21
17
  context: Record<string, unknown>;
22
18
  };
23
- export declare const PropsContext: import("react").Context<PropsContextValue>;
24
19
  export declare const ActionsContext: import("react").Context<ActionsContextValue>;
25
20
  export declare const DataContext: import("react").Context<DataContextValue>;
26
21
  export declare const NavigationContext: import("react").Context<NavigationContextValue>;
@@ -1,17 +1,19 @@
1
1
  import { type Dispatch, type SetStateAction } from 'react';
2
- import type { BlockerState, LoaderState, Location, RevalidateCacheArgs, RouteItem, UpdateBlockedRouteProps } from '../types/global';
2
+ import { BlockerState, Location, RevalidateCacheArgs, RouteItem, RouteItemData, UpdateBlockedRouteProps } from '../types/global';
3
3
  type UseHandleNavigation = {
4
4
  routeList: RouteItem[];
5
- setLocation: (arg: Location) => void;
6
5
  context: Record<string, unknown>;
7
6
  revalidateCache(arg: RevalidateCacheArgs): Promise<void>;
8
7
  setContext: Dispatch<SetStateAction<Record<string, unknown>>>;
9
- setLoaderState: Dispatch<SetStateAction<LoaderState>>;
8
+ setIsLoading: Dispatch<SetStateAction<boolean>>;
10
9
  };
11
- export declare const useHandleNavigation: ({ setLocation, routeList, context, revalidateCache, setContext, setLoaderState, }: UseHandleNavigation) => {
10
+ export declare const useHandleNavigation: ({ routeList, context, revalidateCache, setContext, setIsLoading, }: UseHandleNavigation) => {
12
11
  blockerState: BlockerState;
13
12
  updateLocation: (nextLocation: Location) => Promise<void>;
14
13
  updateBlockedRoute: ({ type, payload }: UpdateBlockedRouteProps) => void;
15
- nextItem: RouteItem | undefined;
14
+ routeItemData: RouteItemData;
15
+ setSearch: (search: string) => void;
16
+ restoreScroll: () => void;
17
+ currentLoaderFallback: import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | (() => import("react").ReactElement) | undefined;
16
18
  };
17
19
  export {};
@@ -1,14 +1,14 @@
1
1
  import { type Dispatch, type SetStateAction } from 'react';
2
- import type { LoaderState, RevalidateCacheArgs, RouteItem } from '../types/global';
2
+ import type { RevalidateCacheArgs, RouteItem } from '../types/global';
3
3
  type UseLoaderParams = {
4
4
  routeList: RouteItem[];
5
5
  context: Record<string, unknown>;
6
6
  setContext: Dispatch<SetStateAction<Record<string, unknown>>>;
7
- setLoaderState: Dispatch<SetStateAction<LoaderState>>;
8
7
  };
9
- export declare const useLoader: ({ routeList, context, setContext, setLoaderState }: UseLoaderParams) => {
8
+ export declare const useLoader: ({ routeList, context, setContext }: UseLoaderParams) => {
10
9
  prefetchLoader: (pathname: string) => Promise<void>;
11
- revalidateCache: ({ routeItem, isCurrentRoute, pathname }: RevalidateCacheArgs) => Promise<void>;
10
+ revalidateCache: ({ routeItem, loaderState, pathname }: RevalidateCacheArgs) => Promise<void>;
12
11
  isLoading: boolean;
12
+ setIsLoading: Dispatch<SetStateAction<boolean>>;
13
13
  };
14
14
  export {};
@@ -1,7 +1 @@
1
- type UsePreserveScrollParams = {
2
- pathname: string;
3
- preserveScroll: boolean;
4
- nextPathname?: string;
5
- };
6
- export declare const usePreserveScroll: ({ pathname, preserveScroll, nextPathname }: UsePreserveScrollParams) => void;
7
- export {};
1
+ export declare const usePreserveScroll: (preserveScroll: boolean) => void;
@@ -1,4 +1,3 @@
1
1
  export declare const useNavigationState: () => import("../context/RouterProviderContext").NavigationContextValue;
2
2
  export declare const useRouterActions: () => import("../context/RouterProviderContext").ActionsContextValue;
3
3
  export declare const useRouterData: () => import("../context/RouterProviderContext").DataContextValue;
4
- export declare const usePropsData: () => import("../context/RouterProviderContext").PropsContextValue;
package/dist/index.js CHANGED
@@ -3,7 +3,6 @@ import { Suspense, createContext, lazy, useCallback, useContext, useEffect, useM
3
3
  var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
4
4
  //#endregion
5
5
  //#region context/RouterProviderContext.ts
6
- var PropsContext = createContext({});
7
6
  var ActionsContext = createContext({});
8
7
  var DataContext = createContext({});
9
8
  var NavigationContext = createContext({});
@@ -19,7 +18,7 @@ var NavigationContext = createContext({});
19
18
  * LICENSE file in the root directory of this source tree.
20
19
  */
21
20
  var require_react_jsx_runtime_production = /* @__PURE__ */ __commonJSMin(((exports) => {
22
- var REACT_ELEMENT_TYPE = Symbol.for("react.transitional.element");
21
+ var REACT_ELEMENT_TYPE = Symbol.for("react.transitional.element"), REACT_FRAGMENT_TYPE = Symbol.for("react.fragment");
23
22
  function jsxProd(type, config, maybeKey) {
24
23
  var key = null;
25
24
  void 0 !== maybeKey && (key = "" + maybeKey);
@@ -37,6 +36,7 @@ var require_react_jsx_runtime_production = /* @__PURE__ */ __commonJSMin(((expor
37
36
  props: maybeKey
38
37
  };
39
38
  }
39
+ exports.Fragment = REACT_FRAGMENT_TYPE;
40
40
  exports.jsx = jsxProd;
41
41
  exports.jsxs = jsxProd;
42
42
  }));
@@ -45,31 +45,26 @@ var require_react_jsx_runtime_production = /* @__PURE__ */ __commonJSMin(((expor
45
45
  var import_jsx_runtime = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
46
46
  module.exports = require_react_jsx_runtime_production();
47
47
  })))();
48
- var Provider = ({ children, setContext, context, updateBlockedRoute, updateLocation, location, setLocation, prefetchLoader, loaderState, blockerState, routeList, isLoading, nextItem }) => {
49
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PropsContext.Provider, {
50
- value: { routeList },
51
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ActionsContext.Provider, {
52
- value: {
53
- setLocation,
54
- updateLocation,
55
- updateBlockedRoute,
56
- prefetchLoader,
57
- setContext
58
- },
59
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DataContext.Provider, {
48
+ var Provider = ({ children, setContext, context, updateBlockedRoute, updateLocation, setSearch, prefetchLoader, blockerState, isLoading, routeItemData, restoreScroll, currentLoaderFallback }) => {
49
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ActionsContext.Provider, {
50
+ value: {
51
+ setSearch,
52
+ updateLocation,
53
+ updateBlockedRoute,
54
+ prefetchLoader,
55
+ setContext,
56
+ restoreScroll
57
+ },
58
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DataContext.Provider, {
59
+ value: { context },
60
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(NavigationContext.Provider, {
60
61
  value: {
61
- context,
62
- loaderState
62
+ blockerState,
63
+ isLoading,
64
+ routeItemData,
65
+ currentLoaderFallback
63
66
  },
64
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(NavigationContext.Provider, {
65
- value: {
66
- blockerState,
67
- location,
68
- isLoading,
69
- nextItem
70
- },
71
- children
72
- })
67
+ children
73
68
  })
74
69
  })
75
70
  });
@@ -113,10 +108,10 @@ var parseClientRouteItem = (el, parentParams = [], parentPath = "") => {
113
108
  }, ...el.children?.flatMap((child) => parseClientRouteItem(child, currentParams, path)) || []];
114
109
  };
115
110
  var createRouter = (clientList) => clientList.flatMap((el) => parseClientRouteItem(el, []));
116
- var getParamsObject = ({ routeItem, pathname }) => {
117
- if (!routeItem?.params) return {};
111
+ var getParamsObject = ({ params, pathname }) => {
112
+ if (!params) return {};
118
113
  const split = pathname.split("/");
119
- return (routeItem.params || []).map((el) => ({
114
+ return (params || []).map((el) => ({
120
115
  index: split.findIndex((item) => item === el.key),
121
116
  value: el.value
122
117
  })).reduce((acc, cur) => ({
@@ -136,71 +131,103 @@ var comparePaths = (el, pathname) => {
136
131
  };
137
132
  //#endregion
138
133
  //#region hooks/useHandleNavigation.ts
139
- var useHandleNavigation = ({ setLocation, routeList, context, revalidateCache, setContext, setLoaderState }) => {
134
+ var ALL_LOCATIONS = "*";
135
+ var useHandleNavigation = ({ routeList, context, revalidateCache, setContext, setIsLoading }) => {
140
136
  const [blockedRoute, setBlockedRoute] = useState({
141
137
  from: "",
142
138
  to: ""
143
139
  });
144
- const [nextItem, setNextItem] = useState();
140
+ const [routeItemData, setRouteItemData] = useState({
141
+ location: {},
142
+ routeItem: void 0,
143
+ loaderState: {}
144
+ });
145
+ const [scrollMap, setScrollMap] = useState({});
146
+ const [currentLoaderFallback, setCurrentLoaderFallback] = useState();
147
+ const loaderState = useRef({});
145
148
  const prevPathname = useRef("");
146
149
  const navigationSeq = useRef(0);
147
- const navigation = useCallback((nextLocation) => {
148
- setLocation(nextLocation);
150
+ const scrollMapRef = useLatest(scrollMap);
151
+ const setSearch = useCallback((search) => setRouteItemData((prevState) => ({
152
+ ...prevState,
153
+ location: {
154
+ ...prevState.location,
155
+ search
156
+ }
157
+ })), []);
158
+ const restoreScroll = useCallback(() => {
159
+ if (!prevPathname.current || !scrollMapRef.current[prevPathname.current]) return;
160
+ requestAnimationFrame(() => {
161
+ window.scrollTo({
162
+ top: scrollMapRef.current[prevPathname.current],
163
+ behavior: "smooth"
164
+ });
165
+ });
166
+ }, [scrollMapRef]);
167
+ const navigation = useCallback((nextLocation, routeItem) => {
168
+ setRouteItemData({
169
+ location: nextLocation,
170
+ routeItem,
171
+ loaderState: loaderState.current
172
+ });
173
+ setIsLoading(false);
149
174
  prevPathname.current = nextLocation.pathname;
150
175
  const fullPath = nextLocation.search ? `${nextLocation.pathname}${nextLocation.search}` : nextLocation.pathname;
151
176
  if (fullPath === window.location.pathname + window.location.search) return;
152
177
  history.pushState(null, "", fullPath);
153
- }, [setLocation]);
154
- const transitionedNavigation = useCallback((nextLocation) => {
178
+ }, [setIsLoading]);
179
+ const transitionedNavigation = useCallback((nextLocation, routeItem) => {
180
+ setScrollMap((prevState) => {
181
+ const scrollPosition = document.scrollingElement?.scrollTop ?? 0;
182
+ if (!scrollPosition || prevState[prevPathname.current] === scrollPosition) return prevState;
183
+ return {
184
+ ...prevState,
185
+ [prevPathname.current]: scrollPosition
186
+ };
187
+ });
155
188
  try {
156
- document.startViewTransition(() => navigation(nextLocation));
189
+ document.startViewTransition(() => navigation(nextLocation, routeItem));
157
190
  } catch {
158
- navigation(nextLocation);
191
+ navigation(nextLocation, routeItem);
159
192
  }
160
193
  }, [navigation]);
161
194
  const navigationHandler = useCallback(async (nextLocation) => {
162
195
  navigationSeq.current = navigationSeq.current + 1;
163
196
  const seq = navigationSeq.current;
164
- const nextItem = routeList.find((el) => comparePaths(el, nextLocation.pathname));
165
- setNextItem(nextItem);
197
+ loaderState.current = {};
198
+ const nextItem = routeList.find((el) => el.path === ALL_LOCATIONS || comparePaths(el, nextLocation.pathname));
166
199
  const params = getParamsObject({
167
- routeItem: nextItem,
200
+ params: nextItem?.params,
168
201
  pathname: nextLocation.pathname
169
202
  });
170
203
  if (nextItem?.beforeLoad) try {
171
- const redirect = async (location) => typeof location === "string" ? await navigationHandler({ pathname: location }) : await navigationHandler(location);
204
+ const redirect = async (location) => await navigationHandler(typeof location === "string" ? { pathname: location } : location);
172
205
  await nextItem.beforeLoad({
173
206
  context,
174
207
  redirect,
175
208
  params,
176
209
  setContext
177
210
  });
178
- setLoaderState((prevState) => ({
179
- ...prevState,
180
- [nextLocation.pathname]: {
181
- ...prevState[nextLocation.pathname],
182
- beforeLoadError: null
183
- }
184
- }));
211
+ loaderState.current = {
212
+ ...loaderState.current,
213
+ beforeLoadError: null
214
+ };
185
215
  } catch (error) {
186
- setLoaderState((prevState) => ({
187
- ...prevState,
188
- [nextLocation.pathname]: {
189
- ...prevState[nextLocation.pathname],
190
- beforeLoadError: error
191
- }
192
- }));
193
- transitionedNavigation(nextLocation);
194
- return;
216
+ loaderState.current = {
217
+ ...loaderState.current,
218
+ beforeLoadError: error
219
+ };
220
+ return transitionedNavigation(nextLocation, nextItem);
195
221
  }
196
222
  if (seq !== navigationSeq.current) return;
223
+ setCurrentLoaderFallback(nextItem?.loaderFallback);
197
224
  await revalidateCache({
198
225
  routeItem: nextItem,
199
- isCurrentRoute: true,
226
+ loaderState,
200
227
  pathname: nextLocation.pathname
201
228
  });
202
229
  if (seq !== navigationSeq.current) return;
203
- transitionedNavigation(nextLocation);
230
+ transitionedNavigation(nextLocation, nextItem);
204
231
  if (nextItem?.afterLoad) await nextItem.afterLoad({
205
232
  context,
206
233
  params,
@@ -211,8 +238,7 @@ var useHandleNavigation = ({ setLocation, routeList, context, revalidateCache, s
211
238
  revalidateCache,
212
239
  routeList,
213
240
  transitionedNavigation,
214
- setContext,
215
- setLoaderState
241
+ setContext
216
242
  ]);
217
243
  const setNextLocationRef = useLatest(navigationHandler);
218
244
  const updateBlockedRoute = useCallback(({ type, payload = "" }) => setBlockedRoute((prevState) => {
@@ -266,14 +292,18 @@ var useHandleNavigation = ({ setLocation, routeList, context, revalidateCache, s
266
292
  }, [blockedRoute]),
267
293
  updateLocation,
268
294
  updateBlockedRoute,
269
- nextItem
295
+ routeItemData,
296
+ setSearch,
297
+ restoreScroll,
298
+ currentLoaderFallback
270
299
  };
271
300
  };
272
301
  //#endregion
273
302
  //#region hooks/useLoader.ts
274
- var useLoader = ({ routeList, context, setContext, setLoaderState }) => {
303
+ var useLoader = ({ routeList, context, setContext }) => {
275
304
  const [isLoading, setIsLoading] = useState(false);
276
305
  const cacheTimestampsRef = useRef({});
306
+ const loaderCacheRef = useRef({});
277
307
  const isCacheItemFresh = useCallback(({ routeItem, pathname }) => {
278
308
  if (!routeItem) return true;
279
309
  const currentCacheTimestamp = cacheTimestampsRef.current[pathname];
@@ -281,16 +311,22 @@ var useLoader = ({ routeList, context, setContext, setLoaderState }) => {
281
311
  if (!routeItem.staleTime) return true;
282
312
  return Date.now() - currentCacheTimestamp < routeItem.staleTime;
283
313
  }, []);
284
- const revalidateCache = useCallback(async ({ routeItem, isCurrentRoute, pathname }) => {
285
- if (!routeItem?.loader) return;
314
+ const revalidateCache = useCallback(async ({ routeItem, loaderState, pathname }) => {
315
+ if (!routeItem?.loader) {
316
+ if (loaderState) loaderState.current = {};
317
+ return;
318
+ }
286
319
  if (isCacheItemFresh({
287
320
  routeItem,
288
321
  pathname
289
- })) return;
290
- if (isCurrentRoute) setIsLoading(true);
322
+ })) {
323
+ if (loaderState) loaderState.current = loaderCacheRef.current[pathname];
324
+ return;
325
+ }
326
+ if (loaderState) setIsLoading(true);
291
327
  try {
292
328
  const params = getParamsObject({
293
- routeItem,
329
+ params: routeItem.params,
294
330
  pathname
295
331
  });
296
332
  const result = await routeItem?.loader({
@@ -302,31 +338,27 @@ var useLoader = ({ routeList, context, setContext, setLoaderState }) => {
302
338
  ...cacheTimestampsRef.current,
303
339
  [pathname]: Date.now()
304
340
  };
305
- if (isCurrentRoute) setLoaderState((prevState) => ({
306
- ...prevState,
307
- [pathname]: {
308
- ...prevState[pathname],
309
- data: result,
310
- loaderError: null
311
- }
312
- }));
341
+ loaderCacheRef.current[pathname] = {
342
+ data: result,
343
+ loaderError: null,
344
+ beforeLoadError: null
345
+ };
346
+ if (loaderState) loaderState.current = {
347
+ ...loaderState?.current,
348
+ data: result,
349
+ loaderError: null
350
+ };
313
351
  } catch (error) {
314
- if (isCurrentRoute) setLoaderState((prevState) => ({
315
- ...prevState,
316
- [pathname]: {
317
- ...prevState[pathname],
318
- data: null,
319
- loaderError: error
320
- }
321
- }));
322
- } finally {
323
- setTimeout(() => setIsLoading(false), 10);
352
+ if (loaderState) loaderState.current = {
353
+ ...loaderState?.current,
354
+ data: null,
355
+ loaderError: error
356
+ };
324
357
  }
325
358
  }, [
326
359
  context,
327
360
  isCacheItemFresh,
328
- setContext,
329
- setLoaderState
361
+ setContext
330
362
  ]);
331
363
  return {
332
364
  prefetchLoader: useCallback(async (pathname) => {
@@ -337,54 +369,52 @@ var useLoader = ({ routeList, context, setContext, setLoaderState }) => {
337
369
  });
338
370
  }, [revalidateCache, routeList]),
339
371
  revalidateCache,
340
- isLoading
372
+ isLoading,
373
+ setIsLoading
341
374
  };
342
375
  };
343
376
  //#endregion
344
377
  //#region components/RouterProvider.tsx
345
378
  var RouterProvider = ({ children, routeList, context: initialContext = {} }) => {
346
- const [location, setLocation] = useState({});
347
379
  const [context, setContext] = useState(initialContext);
348
- const [loaderState, setLoaderState] = useState({});
349
- const { prefetchLoader, revalidateCache, isLoading } = useLoader({
380
+ const { prefetchLoader, revalidateCache, isLoading, setIsLoading } = useLoader({
350
381
  routeList,
351
382
  context,
352
- setContext,
353
- setLoaderState
383
+ setContext
354
384
  });
355
- const { blockerState, updateLocation, updateBlockedRoute, nextItem } = useHandleNavigation({
356
- setLocation,
385
+ const { blockerState, updateLocation, updateBlockedRoute, routeItemData, setSearch, restoreScroll, currentLoaderFallback } = useHandleNavigation({
357
386
  routeList,
358
387
  context,
359
388
  setContext,
360
389
  revalidateCache,
361
- setLoaderState
390
+ setIsLoading
362
391
  });
363
392
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Provider, {
364
393
  ...useMemo(() => ({
365
- location,
366
- setLocation,
394
+ setSearch,
367
395
  updateLocation,
368
- loaderState,
369
396
  prefetchLoader,
370
397
  updateBlockedRoute,
371
398
  blockerState,
372
399
  context,
373
400
  setContext,
374
401
  routeList,
375
- nextItem,
402
+ routeItemData,
403
+ restoreScroll,
404
+ currentLoaderFallback,
376
405
  isLoading
377
406
  }), [
378
407
  blockerState,
379
408
  context,
380
409
  isLoading,
381
- loaderState,
382
- location,
383
410
  prefetchLoader,
411
+ routeItemData,
384
412
  routeList,
413
+ setSearch,
385
414
  updateBlockedRoute,
386
415
  updateLocation,
387
- nextItem
416
+ currentLoaderFallback,
417
+ restoreScroll
388
418
  ]),
389
419
  children
390
420
  });
@@ -399,7 +429,6 @@ var useServiceState = (reactContext) => {
399
429
  var useNavigationState = () => useServiceState(NavigationContext);
400
430
  var useRouterActions = () => useServiceState(ActionsContext);
401
431
  var useRouterData = () => useServiceState(DataContext);
402
- var usePropsData = () => useServiceState(PropsContext);
403
432
  //#endregion
404
433
  //#region hooks/useApplyCustomAnimation.ts
405
434
  var useApplyCustomAnimation = (animationDuration) => {
@@ -439,44 +468,18 @@ var useApplyCustomAnimation = (animationDuration) => {
439
468
  };
440
469
  //#endregion
441
470
  //#region hooks/usePreserveScroll.ts
442
- var usePreserveScroll = ({ pathname, preserveScroll, nextPathname }) => {
443
- const [scrollMap, setScrollMap] = useState({});
444
- const prevPathname = useRef("");
445
- useEffect(() => {
446
- setScrollMap((prevState) => {
447
- const scrollPosition = document.scrollingElement?.scrollTop ?? 0;
448
- if (!scrollPosition || prevState[prevPathname.current] === scrollPosition) return prevState;
449
- return {
450
- ...prevState,
451
- [prevPathname.current]: scrollPosition
452
- };
453
- });
454
- prevPathname.current = pathname;
455
- }, [pathname, nextPathname]);
471
+ var usePreserveScroll = (preserveScroll) => {
472
+ const { restoreScroll } = useRouterActions();
473
+ const { routeItemData: { location: { pathname } } } = useNavigationState();
456
474
  useEffect(() => {
457
- if (!preserveScroll || !pathname || !scrollMap[pathname]) return;
458
- document.scrollingElement?.scrollTo({
459
- top: scrollMap[pathname],
460
- behavior: "smooth"
461
- });
475
+ if (preserveScroll) restoreScroll();
462
476
  }, [
463
- pathname,
464
- scrollMap,
465
- preserveScroll
477
+ preserveScroll,
478
+ restoreScroll,
479
+ pathname
466
480
  ]);
467
481
  };
468
482
  //#endregion
469
- //#region context/RouterViewContext.ts
470
- var RouterViewContext = createContext({});
471
- //#endregion
472
- //#region provider/ViewProvider.tsx
473
- var ViewProvider = ({ children, params }) => {
474
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RouterViewContext.Provider, {
475
- value: params,
476
- children
477
- });
478
- };
479
- //#endregion
480
483
  //#region components/Spinner.tsx
481
484
  var Spinner = () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "cr-spinner" });
482
485
  //#endregion
@@ -487,57 +490,27 @@ var renderElement = (Component) => {
487
490
  };
488
491
  //#endregion
489
492
  //#region components/Router.tsx
490
- var ALL_LOCATIONS = "*";
491
- var Router = ({ isAnimated, animationDuration, spinner = true, preserveScroll = true }) => {
492
- const { location, isLoading, nextItem } = useNavigationState();
493
- const { routeList } = usePropsData();
494
- const { loaderState } = useRouterData();
495
- usePreserveScroll({
496
- nextPathname: nextItem?.path,
497
- pathname: location.pathname,
498
- preserveScroll
499
- });
493
+ var Router = ({ isAnimated, animationDuration, spinner = true, preserveScroll = true, showFallbackIfAnimated = false }) => {
494
+ const { isLoading, routeItemData: { routeItem, loaderState }, currentLoaderFallback } = useNavigationState();
495
+ usePreserveScroll(preserveScroll);
500
496
  useApplyCustomAnimation(animationDuration);
501
- const routeItem = useMemo(() => {
502
- if (!location.pathname) return void 0;
503
- return routeList.find((el) => el.path === ALL_LOCATIONS || comparePaths(el, location.pathname));
504
- }, [location.pathname, routeList]);
505
- const params = useMemo(() => {
506
- return getParamsObject({
507
- routeItem: routeItem || nextItem,
508
- pathname: location.pathname || window.location.pathname
509
- });
510
- }, [
511
- location.pathname,
512
- routeItem,
513
- nextItem
514
- ]);
515
- const shouldErrorElementShown = useMemo(() => Boolean(loaderState[location.pathname]?.loaderError || loaderState[location.pathname]?.beforeLoadError), [loaderState, location.pathname]);
516
- if (!routeItem && !nextItem) return null;
517
- if (!routeItem && nextItem) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ViewProvider, {
518
- params,
519
- children: renderElement(nextItem?.loaderFallback)
520
- });
521
- if (!isAnimated && !shouldErrorElementShown && isLoading) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ViewProvider, {
522
- params,
523
- children: renderElement(nextItem?.loaderFallback)
524
- });
525
- if (shouldErrorElementShown) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(ViewProvider, {
526
- params,
527
- children: [renderElement(routeItem?.errorElement), spinner && isAnimated && isLoading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {})]
528
- });
529
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
497
+ const showErrorElement = useMemo(() => Boolean(loaderState.loaderError || loaderState.beforeLoadError), [loaderState]);
498
+ const showSpinner = spinner && isAnimated && isLoading;
499
+ const loadingContent = !showErrorElement && isLoading;
500
+ if ((showFallbackIfAnimated || !isAnimated) && loadingContent) return renderElement(currentLoaderFallback);
501
+ if (!showFallbackIfAnimated && isAnimated && loadingContent) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {});
502
+ if (!routeItem) return null;
503
+ 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, {})] });
504
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
530
505
  style: { viewTransitionName: "page" },
531
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(ViewProvider, {
532
- params,
533
- children: [renderElement(routeItem?.element) || null, spinner && isAnimated && isLoading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {})]
534
- })
506
+ children: [renderElement(routeItem.element) || null, showSpinner && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {})]
535
507
  });
536
508
  };
537
509
  //#endregion
538
510
  //#region hooks/useLocation.ts
539
511
  var useLocation = () => {
540
- return useNavigationState().location;
512
+ const { routeItemData: { location } } = useNavigationState();
513
+ return location;
541
514
  };
542
515
  //#endregion
543
516
  //#region hooks/useNavigate.ts
@@ -584,19 +557,24 @@ var Link = ({ children, to, prefetch = true, prefetchDelay = STANDARD_DELAY }) =
584
557
  //#endregion
585
558
  //#region hooks/useParams.ts
586
559
  var useParams = () => {
587
- return useContext(RouterViewContext);
560
+ const { routeItemData: { routeItem } } = useNavigationState();
561
+ if (!routeItem) return void 0;
562
+ return getParamsObject({
563
+ params: routeItem?.params,
564
+ pathname: location.pathname
565
+ });
588
566
  };
589
567
  //#endregion
590
568
  //#region hooks/useLoaderState.ts
591
569
  var useLoaderState = () => {
592
- const { pathname } = useLocation();
593
- const { loaderState } = useRouterData();
594
- return loaderState[pathname];
570
+ const { routeItemData: { loaderState } } = useNavigationState();
571
+ return loaderState;
595
572
  };
596
573
  //#endregion
597
574
  //#region hooks/useBlocker.ts
598
575
  var useBlocker = (blockerFn) => {
599
- const { location: { pathname }, blockerState } = useNavigationState();
576
+ const { blockerState } = useNavigationState();
577
+ const { routeItemData: { location: { pathname } } } = useNavigationState();
600
578
  const { updateBlockedRoute } = useRouterActions();
601
579
  const shouldBlock = blockerFn();
602
580
  useEffect(() => updateBlockedRoute(shouldBlock ? {
@@ -639,8 +617,8 @@ var useRouterContext = () => {
639
617
  //#endregion
640
618
  //#region hooks/useSearchParams.ts
641
619
  var useSearchParams = () => {
642
- const location = useLocation();
643
- const { setLocation } = useRouterActions();
620
+ const { routeItemData: { location } } = useNavigationState();
621
+ const { setSearch } = useRouterActions();
644
622
  const locationRef = useLatest(location);
645
623
  const searchString = useMemo(() => location.search ? location.search.replace("?", "") : location.pathname.split("?")?.[1] ?? "", [location.pathname, location.search]);
646
624
  const searchParams = useMemo(() => new URLSearchParams(searchString), [searchString]);
@@ -652,12 +630,9 @@ var useSearchParams = () => {
652
630
  const newSearch = params.toString();
653
631
  const { pathname } = locationRef.current;
654
632
  const search = newSearch ? `?${newSearch}` : "";
655
- setLocation((prevState) => ({
656
- ...prevState,
657
- search
658
- }));
633
+ setSearch(search);
659
634
  history.replaceState(null, "", pathname + search);
660
- }, [locationRef, setLocation]);
635
+ }, [locationRef, setSearch]);
661
636
  return {
662
637
  searchParams,
663
638
  getSearchParams,
@@ -1,7 +1,7 @@
1
1
  import { type ReactNode } from 'react';
2
- import { type ActionsContextValue, type DataContextValue, type NavigationContextValue, PropsContextValue } from '../context/RouterProviderContext';
3
- type ProviderProps = PropsContextValue & NavigationContextValue & ActionsContextValue & DataContextValue & {
2
+ import { type ActionsContextValue, type DataContextValue, type NavigationContextValue } from '../context/RouterProviderContext';
3
+ type ProviderProps = NavigationContextValue & ActionsContextValue & DataContextValue & {
4
4
  children: ReactNode;
5
5
  };
6
- export declare const Provider: ({ children, setContext, context, updateBlockedRoute, updateLocation, location, setLocation, prefetchLoader, loaderState, blockerState, routeList, isLoading, nextItem, }: ProviderProps) => import("react/jsx-runtime").JSX.Element;
6
+ export declare const Provider: ({ children, setContext, context, updateBlockedRoute, updateLocation, setSearch, prefetchLoader, blockerState, isLoading, routeItemData, restoreScroll, currentLoaderFallback, }: ProviderProps) => import("react/jsx-runtime").JSX.Element;
7
7
  export {};
@@ -1,4 +1,4 @@
1
- import type { ComponentType, Dispatch, ReactElement, SetStateAction } from 'react';
1
+ import type { ComponentType, Dispatch, ReactElement, RefObject, SetStateAction } from 'react';
2
2
  export type LazyComponent = () => Promise<{
3
3
  default: ComponentType<unknown>;
4
4
  }>;
@@ -48,14 +48,19 @@ export type UpdateBlockedRouteProps = {
48
48
  export type RevalidateCacheArgs = {
49
49
  pathname: string;
50
50
  routeItem?: RouteItem;
51
- isCurrentRoute?: boolean;
51
+ loaderState?: RefObject<LoaderState>;
52
52
  };
53
- export type LoaderState = Record<string, {
53
+ export type LoaderState = {
54
54
  data: unknown;
55
55
  loaderError: Error | null;
56
56
  beforeLoadError: Error | null;
57
- }>;
57
+ };
58
58
  export type Adapter<T> = {
59
59
  parse: (params: string[]) => T;
60
60
  serialize?: (params: T) => string | string[];
61
61
  };
62
+ export type RouteItemData = {
63
+ location: Location;
64
+ routeItem: RouteItem | undefined;
65
+ loaderState: LoaderState;
66
+ };
@@ -1,7 +1,7 @@
1
1
  import type { ClientRouteItem, Location, RouteItem } from '../types/global';
2
2
  export declare const createRouter: (clientList: ClientRouteItem[]) => RouteItem[];
3
- export declare const getParamsObject: ({ routeItem, pathname }: {
4
- routeItem?: RouteItem;
3
+ export declare const getParamsObject: ({ params, pathname }: {
4
+ params?: RouteItem["params"];
5
5
  pathname: string;
6
6
  }) => {};
7
7
  export declare const parseWindowLocation: (location: typeof window.location) => Location;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clear-react-router",
3
- "version": "1.4.5",
3
+ "version": "1.4.7",
4
4
  "description": "A lightweight, type-safe routing library for React applications",
5
5
  "author": "Andrew Bubnov",
6
6
  "scripts": {
@@ -1 +0,0 @@
1
- export declare const RouterViewContext: import("react").Context<Record<string, string>>;
@@ -1,7 +0,0 @@
1
- import { type ReactNode } from 'react';
2
- type ViewProviderProps = {
3
- params: Record<string, string>;
4
- children: ReactNode;
5
- };
6
- export declare const ViewProvider: ({ children, params }: ViewProviderProps) => import("react/jsx-runtime").JSX.Element;
7
- export {};