clear-react-router 1.3.2 → 1.3.4

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
@@ -12,6 +12,7 @@ A lightweight, type-safe routing library for React applications with nested rout
12
12
  - 🎯 **Type-safe Redirects** - Redirect from beforeLoad hook
13
13
  - 📦 **Prefetching** - Preload data on hover for instant navigation
14
14
  - 🚀 **Lazy Loading** - Code-split your routes with dynamic imports for optimal performance
15
+ - 📍 **Scroll Restoration** — Automatically saves and restores scroll position when navigating back to a page (preserves user's scroll position)
15
16
  - 🎨 **Flexible API** - Use components or hooks as you prefer
16
17
  - 📱 **Browser History** - Full support for browser back/forward buttons
17
18
  - 🧠 **Context-aware** - Pass and update context through routes
@@ -26,9 +27,9 @@ Normalizes route configuration. Handles wildcard `*` routes, extracts dynamic pa
26
27
  |----------|------|-------------|
27
28
  | `path` | `string` | Route path, e.g., `/user/:userId` |
28
29
  | `element` | `ReactElement \| () => ReactElement \| LazyComponent` | Component to render |
29
- | `loader` | `({ params, context }) => Promise<unknown>` | Fetch data using route params and context |
30
- | `beforeLoad` | `({ params, context, redirect }) => Promise<unknown> \| undefined` | Auth checks and redirects. `redirect` is provided by the router |
31
- | `afterLoad` | `({ params, context }) => Promise<void>` | Analytics, side effects after data is loaded |
30
+ | `loader` | `({ params, context, setContext }) => Promise<unknown>` | Fetch data using route params and context. Can update context via `setContext` |
31
+ | `beforeLoad` | `({ params, context, redirect, setContext }) => Promise<unknown> \| undefined \| void` | Auth checks and redirects. Can update context via `setContext`. `redirect` is provided by the router |
32
+ | `afterLoad` | `({ params, context, setContext }) => Promise<void>` | Analytics, side effects after data is loaded. Can update context via `setContext` |
32
33
  | `fallback` | `ReactElement \| () => ReactElement` | Loading fallback (for lazy loading) |
33
34
  | `loaderFallback` | `ReactElement \| () => ReactElement` | Loading fallback (for loader) |
34
35
  | `errorElement` | `ReactElement \| () => ReactElement` | Error fallback |
@@ -45,6 +46,7 @@ The root component that provides routing context to the application. Place stati
45
46
  | `context` | `object` | `{}` | Initial context (user, theme, etc.) |
46
47
  | `isAnimated` | `boolean` | `false` | Enable smooth page transitions |
47
48
  | `animationDuration` | `number` | `optional` | Animation duration in milliseconds (browser default is used if not set) |
49
+ | `preserveScroll` | `boolean` | `true` | Save and restore scroll position when navigating between pages |
48
50
  | `children` | `ReactNode` | required | App content (must include `<Router />`) |
49
51
 
50
52
  ```
@@ -95,7 +97,7 @@ Function provided to `beforeLoad` for programmatic redirection.
95
97
  **Type:** `(arg: Location | string) => Promise<void>`
96
98
 
97
99
  ```
98
- import type { Location } from 'clear-react-router';
100
+ import type { createRouter } from 'clear-react-router';
99
101
 
100
102
  const routes = createRouter([
101
103
  {
@@ -107,8 +109,6 @@ const routes = createRouter([
107
109
  },
108
110
  ]);
109
111
 
110
- or
111
-
112
112
  const routes = createRouter([
113
113
  {
114
114
  path: '/dashboard',
@@ -118,6 +118,21 @@ const routes = createRouter([
118
118
  },
119
119
  },
120
120
  ]);
121
+
122
+ const routes = createRouter([
123
+ {
124
+ path: '/user/:userId',
125
+ loader: async ({ params, context, setContext }) => {
126
+ const user = await fetchUser(params.userId);
127
+ setContext({ ...context, currentUser: user });
128
+ return { user };
129
+ },
130
+ beforeLoad: async ({ context, setContext, redirect }) => {
131
+ if (!context.token) return redirect('/login');
132
+ setContext({ ...context, lastVisit: Date.now() });
133
+ },
134
+ }
135
+ ]);
121
136
 
122
137
  ```
123
138
 
@@ -6,6 +6,7 @@ type RouteProviderProps = {
6
6
  context?: Record<string, unknown>;
7
7
  isAnimated?: boolean;
8
8
  animationDuration?: number;
9
+ preserveScroll?: boolean;
9
10
  };
10
- export declare const RouterProvider: ({ children, routeList, context: initialContext, isAnimated, animationDuration, }: RouteProviderProps) => import("react/jsx-runtime").JSX.Element;
11
+ export declare const RouterProvider: ({ children, routeList, context: initialContext, isAnimated, animationDuration, preserveScroll, }: RouteProviderProps) => import("react/jsx-runtime").JSX.Element;
11
12
  export {};
@@ -1,3 +1,4 @@
1
+ import { type Dispatch, type SetStateAction } from 'react';
1
2
  import type { BlockerState, Location, RevalidateCacheArgs, RouteItem, UpdateBlockedRouteProps } from '../types/global';
2
3
  type UseHandleNavigation = {
3
4
  routeList: RouteItem[];
@@ -5,8 +6,10 @@ type UseHandleNavigation = {
5
6
  context: Record<string, unknown>;
6
7
  revalidateCache(arg: RevalidateCacheArgs): Promise<void>;
7
8
  isAnimated: boolean;
9
+ setContext: Dispatch<SetStateAction<Record<string, unknown>>>;
10
+ setScrollMap: Dispatch<SetStateAction<Record<string, number>>>;
8
11
  };
9
- export declare const useHandleNavigation: ({ setLocation, routeList, context, revalidateCache, isAnimated, }: UseHandleNavigation) => {
12
+ export declare const useHandleNavigation: ({ setLocation, routeList, context, revalidateCache, isAnimated, setContext, setScrollMap, }: UseHandleNavigation) => {
10
13
  blockerState: BlockerState;
11
14
  updateLocation: (nextLocation: Location) => Promise<void>;
12
15
  updateBlockedRoute: ({ type, payload }: UpdateBlockedRouteProps) => void;
@@ -1,9 +1,11 @@
1
+ import { type Dispatch, type SetStateAction } from 'react';
1
2
  import type { RevalidateCacheArgs, RouteItem } from '../types/global';
2
3
  type UseLoaderParams = {
3
4
  routeList: RouteItem[];
4
5
  context: Record<string, unknown>;
6
+ setContext: Dispatch<SetStateAction<Record<string, unknown>>>;
5
7
  };
6
- export declare const useLoader: ({ routeList, context }: UseLoaderParams) => {
8
+ export declare const useLoader: ({ routeList, context, setContext }: UseLoaderParams) => {
7
9
  loaderCache: unknown;
8
10
  loaderError: boolean;
9
11
  prefetchLoader: (pathname: string) => Promise<void>;
@@ -0,0 +1,6 @@
1
+ type UsePreserveScrollParams = {
2
+ pathname: string;
3
+ preserveScroll: boolean;
4
+ };
5
+ export declare const usePreserveScroll: ({ pathname, preserveScroll }: UsePreserveScrollParams) => import("react").Dispatch<import("react").SetStateAction<Record<string, number>>>;
6
+ export {};
package/dist/index.js CHANGED
@@ -77,6 +77,15 @@ var Provider = ({ children, setContext, context, updateBlockedRoute, updateLocat
77
77
  });
78
78
  };
79
79
  //#endregion
80
+ //#region hooks/useLatest.ts
81
+ var useLatest = (value) => {
82
+ const ref = useRef(value);
83
+ useEffect(() => {
84
+ ref.current = value;
85
+ }, [value]);
86
+ return ref;
87
+ };
88
+ //#endregion
80
89
  //#region utils/createLazyComponent.tsx
81
90
  var createLazyComponent = (importFn, fallback) => {
82
91
  const LazyComp = lazy(() => importFn().then((module) => ({ default: module.default || module })));
@@ -128,17 +137,8 @@ var comparePaths = (el, pathname) => {
128
137
  return splitElementPath.every((item, index) => item === splitPathname[index + (index ? 1 : 0)]) && splitPathname.length === splitElementPath.length + paramsLength;
129
138
  };
130
139
  //#endregion
131
- //#region hooks/useLatest.ts
132
- var useLatest = (value) => {
133
- const ref = useRef(value);
134
- useEffect(() => {
135
- ref.current = value;
136
- }, [value]);
137
- return ref;
138
- };
139
- //#endregion
140
140
  //#region hooks/useHandleNavigation.ts
141
- var useHandleNavigation = ({ setLocation, routeList, context, revalidateCache, isAnimated }) => {
141
+ var useHandleNavigation = ({ setLocation, routeList, context, revalidateCache, isAnimated, setContext, setScrollMap }) => {
142
142
  const [blockedRoute, setBlockedRoute] = useState({
143
143
  from: "",
144
144
  to: ""
@@ -146,6 +146,14 @@ var useHandleNavigation = ({ setLocation, routeList, context, revalidateCache, i
146
146
  const [beforeLoadError, setBeforeLoadError] = useState(false);
147
147
  const prevPathname = useRef("");
148
148
  const navigationSeq = useRef(0);
149
+ const updateScrollMap = useCallback(() => setScrollMap((prevState) => {
150
+ const scrollPosition = document.scrollingElement?.scrollTop ?? 0;
151
+ if (!scrollPosition || prevState[prevPathname.current] === scrollPosition) return prevState;
152
+ return {
153
+ ...prevState,
154
+ [prevPathname.current]: scrollPosition
155
+ };
156
+ }), [setScrollMap]);
149
157
  const navigation = useCallback((nextLocation) => {
150
158
  setLocation(nextLocation);
151
159
  prevPathname.current = nextLocation.pathname;
@@ -164,6 +172,7 @@ var useHandleNavigation = ({ setLocation, routeList, context, revalidateCache, i
164
172
  const navigationHandler = useCallback(async (nextLocation, isFirstCall) => {
165
173
  navigationSeq.current = navigationSeq.current + 1;
166
174
  const seq = navigationSeq.current;
175
+ updateScrollMap();
167
176
  const nextItem = routeList.find((el) => comparePaths(el, nextLocation.pathname));
168
177
  const params = getParamsObject({
169
178
  routeItem: nextItem,
@@ -174,7 +183,8 @@ var useHandleNavigation = ({ setLocation, routeList, context, revalidateCache, i
174
183
  await nextItem.beforeLoad({
175
184
  context,
176
185
  redirect,
177
- params
186
+ params,
187
+ setContext
178
188
  });
179
189
  } catch {
180
190
  setBeforeLoadError(true);
@@ -199,14 +209,17 @@ var useHandleNavigation = ({ setLocation, routeList, context, revalidateCache, i
199
209
  setBeforeLoadError(false);
200
210
  if (nextItem?.afterLoad) await nextItem.afterLoad({
201
211
  context,
202
- params
212
+ params,
213
+ setContext
203
214
  });
204
215
  }, [
205
216
  context,
206
217
  revalidateCache,
207
218
  routeList,
208
219
  transitionedNavigation,
209
- isAnimated
220
+ isAnimated,
221
+ setContext,
222
+ updateScrollMap
210
223
  ]);
211
224
  const setNextLocationRef = useLatest(navigationHandler);
212
225
  const updateBlockedRoute = useCallback(({ type, payload = "" }) => setBlockedRoute((prevState) => {
@@ -265,7 +278,7 @@ var useHandleNavigation = ({ setLocation, routeList, context, revalidateCache, i
265
278
  };
266
279
  //#endregion
267
280
  //#region hooks/useLoader.ts
268
- var useLoader = ({ routeList, context }) => {
281
+ var useLoader = ({ routeList, context, setContext }) => {
269
282
  const [loaderCache, setLoaderCache] = useState();
270
283
  const [loaderError, setLoaderError] = useState(false);
271
284
  const [isLoading, setIsLoading] = useState(false);
@@ -301,7 +314,8 @@ var useLoader = ({ routeList, context }) => {
301
314
  });
302
315
  const result = await routeItem?.loader({
303
316
  params,
304
- context
317
+ context,
318
+ setContext
305
319
  });
306
320
  cacheTimestampsRef.current = {
307
321
  ...cacheTimestampsRef.current,
@@ -317,7 +331,11 @@ var useLoader = ({ routeList, context }) => {
317
331
  } finally {
318
332
  if (isCurrentRoute) setIsLoading(false);
319
333
  }
320
- }, [context, isCacheItemFresh]);
334
+ }, [
335
+ context,
336
+ isCacheItemFresh,
337
+ setContext
338
+ ]);
321
339
  return {
322
340
  loaderCache,
323
341
  loaderError,
@@ -333,6 +351,23 @@ var useLoader = ({ routeList, context }) => {
333
351
  };
334
352
  };
335
353
  //#endregion
354
+ //#region hooks/usePreserveScroll.ts
355
+ var usePreserveScroll = ({ pathname, preserveScroll }) => {
356
+ const [scrollMap, setScrollMap] = useState({});
357
+ useEffect(() => {
358
+ if (!preserveScroll || !pathname || !scrollMap[pathname]) return;
359
+ document.scrollingElement?.scrollTo({
360
+ top: scrollMap[pathname],
361
+ behavior: "smooth"
362
+ });
363
+ }, [
364
+ pathname,
365
+ scrollMap,
366
+ preserveScroll
367
+ ]);
368
+ return setScrollMap;
369
+ };
370
+ //#endregion
336
371
  //#region hooks/useApplyCustomAnimation.ts
337
372
  var useApplyCustomAnimation = (animationDuration) => {
338
373
  useEffect(() => {
@@ -371,20 +406,27 @@ var useApplyCustomAnimation = (animationDuration) => {
371
406
  };
372
407
  //#endregion
373
408
  //#region components/RouterProvider.tsx
374
- var RouterProvider = ({ children, routeList, context: initialContext = {}, isAnimated = false, animationDuration }) => {
409
+ var RouterProvider = ({ children, routeList, context: initialContext = {}, isAnimated = false, animationDuration, preserveScroll = true }) => {
375
410
  const [location, setLocation] = useState({});
376
411
  const [context, setContext] = useState(initialContext);
377
412
  useApplyCustomAnimation(animationDuration);
413
+ const setScrollMap = usePreserveScroll({
414
+ pathname: location.pathname,
415
+ preserveScroll
416
+ });
378
417
  const { loaderError, loaderCache, prefetchLoader, revalidateCache, isLoading } = useLoader({
379
418
  routeList,
380
- context
419
+ context,
420
+ setContext
381
421
  });
382
422
  const { blockerState, updateLocation, updateBlockedRoute, beforeLoadError } = useHandleNavigation({
383
423
  setLocation,
384
424
  routeList,
385
425
  context,
426
+ setContext,
386
427
  revalidateCache,
387
- isAnimated
428
+ isAnimated,
429
+ setScrollMap
388
430
  });
389
431
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Provider, {
390
432
  ...useMemo(() => ({
@@ -1,4 +1,4 @@
1
- import type { ComponentType, ReactElement } from 'react';
1
+ import type { ComponentType, Dispatch, ReactElement, SetStateAction } from 'react';
2
2
  export type LazyComponent = () => Promise<{
3
3
  default: ComponentType<unknown>;
4
4
  }>;
@@ -8,6 +8,7 @@ export type ClientRouteItem = {
8
8
  loader?(arg: {
9
9
  params: Record<string, string>;
10
10
  context: Record<string, unknown>;
11
+ setContext: Dispatch<SetStateAction<Record<string, unknown>>>;
11
12
  }): Promise<unknown>;
12
13
  loaderFallback?: (() => ReactElement) | ReactElement;
13
14
  errorElement?: (() => ReactElement) | ReactElement;
@@ -18,10 +19,12 @@ export type ClientRouteItem = {
18
19
  context: Record<string, unknown>;
19
20
  redirect: (arg: Location | string) => Promise<void>;
20
21
  params: Record<string, string>;
21
- }) => Promise<unknown> | undefined;
22
+ setContext: Dispatch<SetStateAction<Record<string, unknown>>>;
23
+ }) => Promise<unknown> | undefined | void;
22
24
  afterLoad?: (arg: {
23
25
  context: Record<string, unknown>;
24
26
  params: Record<string, string>;
27
+ setContext: Dispatch<SetStateAction<Record<string, unknown>>>;
25
28
  }) => Promise<void>;
26
29
  };
27
30
  export type RouteItem = ClientRouteItem & {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clear-react-router",
3
- "version": "1.3.2",
3
+ "version": "1.3.4",
4
4
  "description": "A lightweight, type-safe routing library for React applications",
5
5
  "author": "Andrew Bubnov",
6
6
  "scripts": {