clear-react-router 1.4.6 → 1.4.8

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,20 @@
1
- import { Dispatch, SetStateAction } from 'react';
2
- import { BlockerState, LoaderState, Location, NextItemData, 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
- isLoading: boolean;
10
- nextItemData: NextItemData;
4
+ routeItemData: RouteItemData;
5
+ currentLoaderFallback: RouteItem['loaderFallback'];
11
6
  };
12
7
  export type ActionsContextValue = {
13
- setLocation: Dispatch<SetStateAction<Location>>;
8
+ setSearch(arg: string): void;
14
9
  updateLocation(route: Location): Promise<void>;
15
10
  updateBlockedRoute(arg: UpdateBlockedRouteProps): void;
16
11
  prefetchLoader(arg: string): Promise<void>;
17
12
  setContext(arg: object): void;
13
+ restoreScroll(): void;
18
14
  };
19
15
  export type DataContextValue = {
20
- loaderState: LoaderState;
21
16
  context: Record<string, unknown>;
22
17
  };
23
- export declare const PropsContext: import("react").Context<PropsContextValue>;
24
18
  export declare const ActionsContext: import("react").Context<ActionsContextValue>;
25
19
  export declare const DataContext: import("react").Context<DataContextValue>;
26
20
  export declare const NavigationContext: import("react").Context<NavigationContextValue>;
@@ -1,17 +1,22 @@
1
1
  import { type Dispatch, type SetStateAction } from 'react';
2
- import { BlockerState, LoaderState, Location, NextItemData, 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
+ isCacheItemFresh(arg: {
9
+ routeItem?: RouteItem;
10
+ pathname: string;
11
+ }): boolean;
10
12
  };
11
- export declare const useHandleNavigation: ({ setLocation, routeList, context, revalidateCache, setContext, setLoaderState, }: UseHandleNavigation) => {
13
+ export declare const useHandleNavigation: ({ routeList, context, revalidateCache, setContext, isCacheItemFresh, }: UseHandleNavigation) => {
12
14
  blockerState: BlockerState;
13
15
  updateLocation: (nextLocation: Location) => Promise<void>;
14
16
  updateBlockedRoute: ({ type, payload }: UpdateBlockedRouteProps) => void;
15
- nextItemData: NextItemData;
17
+ routeItemData: RouteItemData;
18
+ setSearch: (search: string) => void;
19
+ restoreScroll: () => void;
20
+ currentLoaderFallback: import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | (() => import("react").ReactElement) | undefined;
16
21
  };
17
22
  export {};
@@ -1,14 +1,16 @@
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>;
12
- isLoading: boolean;
10
+ revalidateCache: ({ routeItem, loaderState, pathname }: RevalidateCacheArgs) => Promise<void>;
11
+ isCacheItemFresh: ({ routeItem, pathname }: {
12
+ routeItem?: RouteItem;
13
+ pathname: string;
14
+ }) => boolean;
13
15
  };
14
16
  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,25 @@ 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, nextItemData }) => {
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, 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
+ routeItemData,
64
+ currentLoaderFallback
63
65
  },
64
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(NavigationContext.Provider, {
65
- value: {
66
- blockerState,
67
- location,
68
- isLoading,
69
- nextItemData
70
- },
71
- children
72
- })
66
+ children
73
67
  })
74
68
  })
75
69
  });
@@ -136,75 +130,106 @@ var comparePaths = (el, pathname) => {
136
130
  };
137
131
  //#endregion
138
132
  //#region hooks/useHandleNavigation.ts
139
- var useHandleNavigation = ({ setLocation, routeList, context, revalidateCache, setContext, setLoaderState }) => {
133
+ var ALL_LOCATIONS = "*";
134
+ var useHandleNavigation = ({ routeList, context, revalidateCache, setContext, isCacheItemFresh }) => {
140
135
  const [blockedRoute, setBlockedRoute] = useState({
141
136
  from: "",
142
137
  to: ""
143
138
  });
144
- const [nextItemData, setNextItemData] = useState({});
139
+ const [routeItemData, setRouteItemData] = useState({
140
+ location: {},
141
+ routeItem: void 0,
142
+ loaderState: {}
143
+ });
144
+ const [scrollMap, setScrollMap] = useState({});
145
+ const [currentLoaderFallback, setCurrentLoaderFallback] = useState();
146
+ const loaderState = useRef({});
145
147
  const prevPathname = useRef("");
146
148
  const navigationSeq = useRef(0);
147
- const navigation = useCallback((nextLocation) => {
148
- setLocation(nextLocation);
149
+ const scrollMapRef = useLatest(scrollMap);
150
+ const setSearch = useCallback((search) => setRouteItemData((prevState) => ({
151
+ ...prevState,
152
+ location: {
153
+ ...prevState.location,
154
+ search
155
+ }
156
+ })), []);
157
+ const restoreScroll = useCallback(() => {
158
+ if (!prevPathname.current || !scrollMapRef.current[prevPathname.current]) return;
159
+ requestAnimationFrame(() => {
160
+ window.scrollTo({
161
+ top: scrollMapRef.current[prevPathname.current],
162
+ behavior: "smooth"
163
+ });
164
+ });
165
+ }, [scrollMapRef]);
166
+ const navigation = useCallback((nextLocation, routeItem) => {
167
+ setRouteItemData({
168
+ location: nextLocation,
169
+ routeItem,
170
+ loaderState: loaderState.current
171
+ });
172
+ setCurrentLoaderFallback(void 0);
149
173
  prevPathname.current = nextLocation.pathname;
150
174
  const fullPath = nextLocation.search ? `${nextLocation.pathname}${nextLocation.search}` : nextLocation.pathname;
151
175
  if (fullPath === window.location.pathname + window.location.search) return;
152
176
  history.pushState(null, "", fullPath);
153
- }, [setLocation]);
154
- const transitionedNavigation = useCallback((nextLocation) => {
177
+ }, []);
178
+ const transitionedNavigation = useCallback((nextLocation, routeItem) => {
179
+ setScrollMap((prevState) => {
180
+ const scrollPosition = document.scrollingElement?.scrollTop ?? 0;
181
+ if (!scrollPosition || prevState[prevPathname.current] === scrollPosition) return prevState;
182
+ return {
183
+ ...prevState,
184
+ [prevPathname.current]: scrollPosition
185
+ };
186
+ });
155
187
  try {
156
- document.startViewTransition(() => navigation(nextLocation));
188
+ document.startViewTransition(() => navigation(nextLocation, routeItem));
157
189
  } catch {
158
- navigation(nextLocation);
190
+ navigation(nextLocation, routeItem);
159
191
  }
160
192
  }, [navigation]);
161
193
  const navigationHandler = useCallback(async (nextLocation) => {
162
194
  navigationSeq.current = navigationSeq.current + 1;
163
195
  const seq = navigationSeq.current;
164
- const nextItem = routeList.find((el) => comparePaths(el, nextLocation.pathname));
165
- setNextItemData({
166
- loaderFallback: nextItem?.loaderFallback,
167
- params: nextItem?.params,
168
- pathname: nextLocation.pathname
169
- });
196
+ loaderState.current = {};
197
+ const nextItem = routeList.find((el) => el.path === ALL_LOCATIONS || comparePaths(el, nextLocation.pathname));
170
198
  const params = getParamsObject({
171
199
  params: nextItem?.params,
172
200
  pathname: nextLocation.pathname
173
201
  });
174
202
  if (nextItem?.beforeLoad) try {
175
- const redirect = async (location) => typeof location === "string" ? await navigationHandler({ pathname: location }) : await navigationHandler(location);
203
+ const redirect = async (location) => await navigationHandler(typeof location === "string" ? { pathname: location } : location);
176
204
  await nextItem.beforeLoad({
177
205
  context,
178
206
  redirect,
179
207
  params,
180
208
  setContext
181
209
  });
182
- setLoaderState((prevState) => ({
183
- ...prevState,
184
- [nextLocation.pathname]: {
185
- ...prevState[nextLocation.pathname],
186
- beforeLoadError: null
187
- }
188
- }));
210
+ loaderState.current = {
211
+ ...loaderState.current,
212
+ beforeLoadError: null
213
+ };
189
214
  } catch (error) {
190
- setLoaderState((prevState) => ({
191
- ...prevState,
192
- [nextLocation.pathname]: {
193
- ...prevState[nextLocation.pathname],
194
- beforeLoadError: error
195
- }
196
- }));
197
- transitionedNavigation(nextLocation);
198
- return;
215
+ loaderState.current = {
216
+ ...loaderState.current,
217
+ beforeLoadError: error
218
+ };
219
+ return transitionedNavigation(nextLocation, nextItem);
199
220
  }
200
221
  if (seq !== navigationSeq.current) return;
222
+ setCurrentLoaderFallback(isCacheItemFresh({
223
+ routeItem: nextItem,
224
+ pathname: nextLocation.pathname
225
+ }) ? void 0 : nextItem?.loaderFallback);
201
226
  await revalidateCache({
202
227
  routeItem: nextItem,
203
- isCurrentRoute: true,
228
+ loaderState,
204
229
  pathname: nextLocation.pathname
205
230
  });
206
231
  if (seq !== navigationSeq.current) return;
207
- transitionedNavigation(nextLocation);
232
+ transitionedNavigation(nextLocation, nextItem);
208
233
  if (nextItem?.afterLoad) await nextItem.afterLoad({
209
234
  context,
210
235
  params,
@@ -216,7 +241,7 @@ var useHandleNavigation = ({ setLocation, routeList, context, revalidateCache, s
216
241
  routeList,
217
242
  transitionedNavigation,
218
243
  setContext,
219
- setLoaderState
244
+ isCacheItemFresh
220
245
  ]);
221
246
  const setNextLocationRef = useLatest(navigationHandler);
222
247
  const updateBlockedRoute = useCallback(({ type, payload = "" }) => setBlockedRoute((prevState) => {
@@ -270,14 +295,17 @@ var useHandleNavigation = ({ setLocation, routeList, context, revalidateCache, s
270
295
  }, [blockedRoute]),
271
296
  updateLocation,
272
297
  updateBlockedRoute,
273
- nextItemData
298
+ routeItemData,
299
+ setSearch,
300
+ restoreScroll,
301
+ currentLoaderFallback
274
302
  };
275
303
  };
276
304
  //#endregion
277
305
  //#region hooks/useLoader.ts
278
- var useLoader = ({ routeList, context, setContext, setLoaderState }) => {
279
- const [isLoading, setIsLoading] = useState(false);
306
+ var useLoader = ({ routeList, context, setContext }) => {
280
307
  const cacheTimestampsRef = useRef({});
308
+ const loaderCacheRef = useRef({});
281
309
  const isCacheItemFresh = useCallback(({ routeItem, pathname }) => {
282
310
  if (!routeItem) return true;
283
311
  const currentCacheTimestamp = cacheTimestampsRef.current[pathname];
@@ -285,13 +313,19 @@ var useLoader = ({ routeList, context, setContext, setLoaderState }) => {
285
313
  if (!routeItem.staleTime) return true;
286
314
  return Date.now() - currentCacheTimestamp < routeItem.staleTime;
287
315
  }, []);
288
- const revalidateCache = useCallback(async ({ routeItem, isCurrentRoute, pathname }) => {
289
- if (!routeItem?.loader) return;
316
+ const revalidateCache = useCallback(async ({ routeItem, loaderState, pathname }) => {
317
+ if (!routeItem?.loader) {
318
+ if (!loaderState) return;
319
+ if (!routeItem?.beforeLoad) loaderState.current = {};
320
+ return;
321
+ }
290
322
  if (isCacheItemFresh({
291
323
  routeItem,
292
324
  pathname
293
- })) return;
294
- if (isCurrentRoute) setIsLoading(true);
325
+ })) {
326
+ if (loaderState) loaderState.current = loaderCacheRef.current[pathname];
327
+ return;
328
+ }
295
329
  try {
296
330
  const params = getParamsObject({
297
331
  params: routeItem.params,
@@ -306,31 +340,27 @@ var useLoader = ({ routeList, context, setContext, setLoaderState }) => {
306
340
  ...cacheTimestampsRef.current,
307
341
  [pathname]: Date.now()
308
342
  };
309
- if (isCurrentRoute) setLoaderState((prevState) => ({
310
- ...prevState,
311
- [pathname]: {
312
- ...prevState[pathname],
313
- data: result,
314
- loaderError: null
315
- }
316
- }));
343
+ loaderCacheRef.current[pathname] = {
344
+ data: result,
345
+ loaderError: null,
346
+ beforeLoadError: null
347
+ };
348
+ if (loaderState) loaderState.current = {
349
+ ...loaderState?.current,
350
+ data: result,
351
+ loaderError: null
352
+ };
317
353
  } catch (error) {
318
- if (isCurrentRoute) setLoaderState((prevState) => ({
319
- ...prevState,
320
- [pathname]: {
321
- ...prevState[pathname],
322
- data: null,
323
- loaderError: error
324
- }
325
- }));
326
- } finally {
327
- setTimeout(() => setIsLoading(false), 10);
354
+ if (loaderState) loaderState.current = {
355
+ ...loaderState?.current,
356
+ data: null,
357
+ loaderError: error
358
+ };
328
359
  }
329
360
  }, [
330
361
  context,
331
362
  isCacheItemFresh,
332
- setContext,
333
- setLoaderState
363
+ setContext
334
364
  ]);
335
365
  return {
336
366
  prefetchLoader: useCallback(async (pathname) => {
@@ -341,54 +371,49 @@ var useLoader = ({ routeList, context, setContext, setLoaderState }) => {
341
371
  });
342
372
  }, [revalidateCache, routeList]),
343
373
  revalidateCache,
344
- isLoading
374
+ isCacheItemFresh
345
375
  };
346
376
  };
347
377
  //#endregion
348
378
  //#region components/RouterProvider.tsx
349
379
  var RouterProvider = ({ children, routeList, context: initialContext = {} }) => {
350
- const [location, setLocation] = useState({});
351
380
  const [context, setContext] = useState(initialContext);
352
- const [loaderState, setLoaderState] = useState({});
353
- const { prefetchLoader, revalidateCache, isLoading } = useLoader({
381
+ const { prefetchLoader, revalidateCache, isCacheItemFresh } = useLoader({
354
382
  routeList,
355
383
  context,
356
- setContext,
357
- setLoaderState
384
+ setContext
358
385
  });
359
- const { blockerState, updateLocation, updateBlockedRoute, nextItemData } = useHandleNavigation({
360
- setLocation,
386
+ const { blockerState, updateLocation, updateBlockedRoute, routeItemData, setSearch, restoreScroll, currentLoaderFallback } = useHandleNavigation({
361
387
  routeList,
362
388
  context,
363
389
  setContext,
364
390
  revalidateCache,
365
- setLoaderState
391
+ isCacheItemFresh
366
392
  });
367
393
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Provider, {
368
394
  ...useMemo(() => ({
369
- location,
370
- setLocation,
395
+ setSearch,
371
396
  updateLocation,
372
- loaderState,
373
397
  prefetchLoader,
374
398
  updateBlockedRoute,
375
399
  blockerState,
376
400
  context,
377
401
  setContext,
378
402
  routeList,
379
- nextItemData,
380
- isLoading
403
+ routeItemData,
404
+ restoreScroll,
405
+ currentLoaderFallback
381
406
  }), [
382
407
  blockerState,
383
408
  context,
384
- isLoading,
385
- loaderState,
386
- location,
387
409
  prefetchLoader,
410
+ routeItemData,
388
411
  routeList,
412
+ setSearch,
389
413
  updateBlockedRoute,
390
414
  updateLocation,
391
- nextItemData
415
+ currentLoaderFallback,
416
+ restoreScroll
392
417
  ]),
393
418
  children
394
419
  });
@@ -403,7 +428,6 @@ var useServiceState = (reactContext) => {
403
428
  var useNavigationState = () => useServiceState(NavigationContext);
404
429
  var useRouterActions = () => useServiceState(ActionsContext);
405
430
  var useRouterData = () => useServiceState(DataContext);
406
- var usePropsData = () => useServiceState(PropsContext);
407
431
  //#endregion
408
432
  //#region hooks/useApplyCustomAnimation.ts
409
433
  var useApplyCustomAnimation = (animationDuration) => {
@@ -443,46 +467,18 @@ var useApplyCustomAnimation = (animationDuration) => {
443
467
  };
444
468
  //#endregion
445
469
  //#region hooks/usePreserveScroll.ts
446
- var usePreserveScroll = ({ pathname, preserveScroll, nextPathname }) => {
447
- const [scrollMap, setScrollMap] = useState({});
448
- const prevPathname = useRef("");
470
+ var usePreserveScroll = (preserveScroll) => {
471
+ const { restoreScroll } = useRouterActions();
472
+ const { routeItemData: { location: { pathname } } } = useNavigationState();
449
473
  useEffect(() => {
450
- setScrollMap((prevState) => {
451
- const scrollPosition = document.scrollingElement?.scrollTop ?? 0;
452
- if (!scrollPosition || prevState[prevPathname.current] === scrollPosition) return prevState;
453
- return {
454
- ...prevState,
455
- [prevPathname.current]: scrollPosition
456
- };
457
- });
458
- prevPathname.current = pathname;
459
- }, [pathname, nextPathname]);
460
- useEffect(() => {
461
- if (!preserveScroll || !pathname || !scrollMap[pathname]) return;
462
- requestAnimationFrame(() => {
463
- window.scrollTo({
464
- top: scrollMap[pathname],
465
- behavior: "smooth"
466
- });
467
- });
474
+ if (preserveScroll) restoreScroll();
468
475
  }, [
469
- pathname,
470
- scrollMap,
471
- preserveScroll
476
+ preserveScroll,
477
+ restoreScroll,
478
+ pathname
472
479
  ]);
473
480
  };
474
481
  //#endregion
475
- //#region context/RouterViewContext.ts
476
- var RouterViewContext = createContext({});
477
- //#endregion
478
- //#region provider/ViewProvider.tsx
479
- var ViewProvider = ({ children, params }) => {
480
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RouterViewContext.Provider, {
481
- value: params,
482
- children
483
- });
484
- };
485
- //#endregion
486
482
  //#region components/Spinner.tsx
487
483
  var Spinner = () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "cr-spinner" });
488
484
  //#endregion
@@ -493,55 +489,28 @@ var renderElement = (Component) => {
493
489
  };
494
490
  //#endregion
495
491
  //#region components/Router.tsx
496
- var ALL_LOCATIONS = "*";
497
- var Router = ({ isAnimated, animationDuration, spinner = true, preserveScroll = true }) => {
498
- const { location, isLoading, nextItemData } = useNavigationState();
499
- const { routeList } = usePropsData();
500
- const { loaderState } = useRouterData();
501
- usePreserveScroll({
502
- nextPathname: nextItemData.pathname,
503
- pathname: location.pathname,
504
- preserveScroll
505
- });
492
+ var Router = ({ isAnimated, animationDuration, spinner = true, preserveScroll = true, showFallbackIfAnimated = false }) => {
493
+ const { routeItemData: { routeItem, loaderState }, currentLoaderFallback } = useNavigationState();
494
+ usePreserveScroll(preserveScroll);
506
495
  useApplyCustomAnimation(animationDuration);
507
- const routeItem = useMemo(() => {
508
- if (!location.pathname) return void 0;
509
- return routeList.find((el) => el.path === ALL_LOCATIONS || comparePaths(el, location.pathname));
510
- }, [location.pathname, routeList]);
511
- const params = useMemo(() => getParamsObject({
512
- params: nextItemData.params,
513
- pathname: nextItemData.pathname || location.pathname
514
- }), [
515
- location.pathname,
516
- nextItemData.params,
517
- nextItemData.pathname
518
- ]);
519
- const shouldErrorElementShown = useMemo(() => Boolean(loaderState[location.pathname]?.loaderError || loaderState[location.pathname]?.beforeLoadError), [loaderState, location.pathname]);
520
- if (!routeItem && !nextItemData.loaderFallback) return null;
521
- if (!routeItem && nextItemData.loaderFallback) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ViewProvider, {
522
- params,
523
- children: renderElement(nextItemData.loaderFallback)
524
- });
525
- if (!isAnimated && !shouldErrorElementShown && isLoading && nextItemData.loaderFallback) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ViewProvider, {
526
- params,
527
- children: renderElement(nextItemData.loaderFallback)
528
- });
529
- if (shouldErrorElementShown) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(ViewProvider, {
530
- params,
531
- children: [renderElement(routeItem?.errorElement), spinner && isAnimated && isLoading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {})]
532
- });
533
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
496
+ const isLoading = Boolean(currentLoaderFallback);
497
+ const showErrorElement = !isLoading && Boolean(loaderState.loaderError || loaderState.beforeLoadError);
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", {
534
505
  style: { viewTransitionName: "page" },
535
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(ViewProvider, {
536
- params,
537
- children: [renderElement(routeItem?.element) || null, spinner && isAnimated && isLoading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {})]
538
- })
506
+ children: [renderElement(routeItem.element) || null, showSpinner && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {})]
539
507
  });
540
508
  };
541
509
  //#endregion
542
510
  //#region hooks/useLocation.ts
543
511
  var useLocation = () => {
544
- return useNavigationState().location;
512
+ const { routeItemData: { location } } = useNavigationState();
513
+ return location;
545
514
  };
546
515
  //#endregion
547
516
  //#region hooks/useNavigate.ts
@@ -588,19 +557,24 @@ var Link = ({ children, to, prefetch = true, prefetchDelay = STANDARD_DELAY }) =
588
557
  //#endregion
589
558
  //#region hooks/useParams.ts
590
559
  var useParams = () => {
591
- 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
+ });
592
566
  };
593
567
  //#endregion
594
568
  //#region hooks/useLoaderState.ts
595
569
  var useLoaderState = () => {
596
- const { pathname } = useLocation();
597
- const { loaderState } = useRouterData();
598
- return loaderState[pathname];
570
+ const { routeItemData: { loaderState } } = useNavigationState();
571
+ return loaderState;
599
572
  };
600
573
  //#endregion
601
574
  //#region hooks/useBlocker.ts
602
575
  var useBlocker = (blockerFn) => {
603
- const { location: { pathname }, blockerState } = useNavigationState();
576
+ const { blockerState } = useNavigationState();
577
+ const { routeItemData: { location: { pathname } } } = useNavigationState();
604
578
  const { updateBlockedRoute } = useRouterActions();
605
579
  const shouldBlock = blockerFn();
606
580
  useEffect(() => updateBlockedRoute(shouldBlock ? {
@@ -643,8 +617,8 @@ var useRouterContext = () => {
643
617
  //#endregion
644
618
  //#region hooks/useSearchParams.ts
645
619
  var useSearchParams = () => {
646
- const location = useLocation();
647
- const { setLocation } = useRouterActions();
620
+ const { routeItemData: { location } } = useNavigationState();
621
+ const { setSearch } = useRouterActions();
648
622
  const locationRef = useLatest(location);
649
623
  const searchString = useMemo(() => location.search ? location.search.replace("?", "") : location.pathname.split("?")?.[1] ?? "", [location.pathname, location.search]);
650
624
  const searchParams = useMemo(() => new URLSearchParams(searchString), [searchString]);
@@ -656,12 +630,9 @@ var useSearchParams = () => {
656
630
  const newSearch = params.toString();
657
631
  const { pathname } = locationRef.current;
658
632
  const search = newSearch ? `?${newSearch}` : "";
659
- setLocation((prevState) => ({
660
- ...prevState,
661
- search
662
- }));
633
+ setSearch(search);
663
634
  history.replaceState(null, "", pathname + search);
664
- }, [locationRef, setLocation]);
635
+ }, [locationRef, setSearch]);
665
636
  return {
666
637
  searchParams,
667
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, nextItemData, }: ProviderProps) => import("react/jsx-runtime").JSX.Element;
6
+ export declare const Provider: ({ children, setContext, context, updateBlockedRoute, updateLocation, setSearch, prefetchLoader, blockerState, 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,19 +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 NextItemData = {
63
- loaderFallback: RouteItem['loaderFallback'];
64
- params: RouteItem['params'];
65
- pathname: string;
62
+ export type RouteItemData = {
63
+ location: Location;
64
+ routeItem: RouteItem | undefined;
65
+ loaderState: LoaderState;
66
66
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clear-react-router",
3
- "version": "1.4.6",
3
+ "version": "1.4.8",
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 {};