clear-react-router 1.2.2 → 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -25,9 +25,9 @@ Normalizes route configuration. Handles wildcard `*` routes, extracts dynamic pa
25
25
  |----------|------|-------------|
26
26
  | `path` | `string` | Route path, e.g., `/user/:userId` |
27
27
  | `element` | `ReactElement \| () => ReactElement \| LazyComponent` | Component to render |
28
- | `loader` | `() => Promise<unknown>` | Fetch data |
29
- | `beforeLoad` | `({ context, redirect }) => Promise<void>` | Auth checks and redirects. `redirect` is provided by the router |
30
- | `afterLoad` | `(context) => Promise<void>` | Analytics, side effects |
28
+ | `loader` | `({ params, context }) => Promise<unknown>` | Fetch data using route params and context |
29
+ | `beforeLoad` | `({ params, context, redirect }) => Promise<unknown> \| undefined` | Auth checks and redirects. `redirect` is provided by the router |
30
+ | `afterLoad` | `({ params, context }) => Promise<void>` | Analytics, side effects after data is loaded |
31
31
  | `fallback` | `ReactElement \| () => ReactElement` | Loading fallback (for lazy loading) |
32
32
  | `loaderFallback` | `ReactElement \| () => ReactElement` | Loading fallback (for loader) |
33
33
  | `errorElement` | `ReactElement \| () => ReactElement` | Error fallback |
@@ -86,6 +86,38 @@ const routes = createRouter([
86
86
  ]);
87
87
  ```
88
88
 
89
+ ### Usage with Parameters
90
+
91
+ The `loader`, `beforeLoad`, and `afterLoad` hooks receive `params` (extracted from the URL) and `context` as arguments. This allows you to handle route-specific logic directly in the route configuration, keeping your components focused on rendering.
92
+
93
+ ```
94
+ const routes = createRouter([
95
+ {
96
+ path: '/user/:userId',
97
+ element: <UserProfile />,
98
+ loader: async ({ params, context }) => {
99
+ // params.userId is available from the URL
100
+ const user = await fetchUser(params.userId);
101
+ return { user };
102
+ },
103
+ beforeLoad: async ({ params, context, redirect }) => {
104
+ // Authentication check
105
+ if (!context.isAuthorized) {
106
+ return redirect({ pathname: '/login' });
107
+ }
108
+ // Validate parameter
109
+ if (!params.userId || !isValidUserId(params.userId)) {
110
+ return redirect({ pathname: '/users' });
111
+ }
112
+ },
113
+ afterLoad: ({ params, context }) => {
114
+ // Analytics or side effects
115
+ console.log(`User ${params.userId} loaded`);
116
+ },
117
+ },
118
+ ]);
119
+ ```
120
+
89
121
  ## Hooks
90
122
 
91
123
  ### `useNavigate()`
@@ -111,8 +143,10 @@ console.log(state); // { userId: 123 }
111
143
 
112
144
  Returns route parameters object.
113
145
 
146
+ ```
114
147
  const params = useParams<{ userId: string }>();
115
148
  // URL: /user/123 → params.userId === '123'
149
+ ```
116
150
 
117
151
  ### `useLocation()`
118
152
 
@@ -124,6 +158,9 @@ const { pathname, search, state } = useLocation();
124
158
  ### `useLoaderState()`
125
159
 
126
160
  Returns loaderState from current route's loader.
161
+ ```
162
+ const state = useLoaderState();
163
+ ```
127
164
 
128
165
  ### `useBlocker(callback)`
129
166
 
@@ -173,6 +210,7 @@ const onSave = useCallback(() => {
173
210
  // Auto-save when user tries to close/reload the page
174
211
  useBeforeUnload(text ? onSave : undefined);
175
212
  ```
213
+ > **Note:** Pass `undefined` to disable the handler (e.g., if there is no changes).
176
214
 
177
215
  ### `useRouterContext()`
178
216
 
@@ -244,5 +282,5 @@ For older browsers, the router gracefully falls back to regular navigation witho
244
282
  - React 16.6+ (for React.lazy and Suspense)
245
283
  - Use `default` export for your lazy-loaded components
246
284
 
247
- ## License
248
- MIT
285
+ ## License
286
+ MIT
@@ -1,14 +1,15 @@
1
- import type { BlockerState, Location, RouteItem, UpdateBlockedRouteProps } from '../types/global.ts';
1
+ import type { BlockerState, Location, RevalidateCacheArgs, RouteItem, UpdateBlockedRouteProps } from '../types/global';
2
2
  type UseHandleNavigation = {
3
3
  routeList: RouteItem[];
4
4
  setLocation: (arg: Location) => void;
5
5
  context: Record<string, unknown>;
6
- revalidateCache(routeItem?: RouteItem, isCurrentRoute?: boolean): Promise<void>;
6
+ revalidateCache(arg: RevalidateCacheArgs): Promise<void>;
7
7
  isAnimated: boolean;
8
8
  };
9
9
  export declare const useHandleNavigation: ({ setLocation, routeList, context, revalidateCache, isAnimated, }: UseHandleNavigation) => {
10
10
  blockerState: BlockerState;
11
11
  updateLocation: (nextLocation: Location) => Promise<void>;
12
12
  updateBlockedRoute: ({ type, payload }: UpdateBlockedRouteProps) => void;
13
+ beforeLoadError: boolean;
13
14
  };
14
15
  export {};
@@ -1,8 +1,13 @@
1
- import type { RouteItem } from '../types/global.ts';
2
- export declare const useLoader: (routeList: RouteItem[]) => {
1
+ import type { RevalidateCacheArgs, RouteItem } from '../types/global';
2
+ type UseLoaderParams = {
3
+ routeList: RouteItem[];
4
+ context: Record<string, unknown>;
5
+ };
6
+ export declare const useLoader: ({ routeList, context }: UseLoaderParams) => {
3
7
  loaderCache: unknown;
4
8
  loaderError: boolean;
5
9
  prefetchLoader: (pathname: string) => Promise<void>;
6
- revalidateCache: (routeItem?: RouteItem, isCurrentRoute?: boolean) => Promise<void>;
10
+ revalidateCache: ({ routeItem, isCurrentRoute, pathname }: RevalidateCacheArgs) => Promise<void>;
7
11
  isLoading: boolean;
8
12
  };
13
+ export {};
package/dist/index.js CHANGED
@@ -98,10 +98,10 @@ var parseClientRouteItem = (el, parentParams = [], parentPath = "") => {
98
98
  }, ...el.children?.flatMap((child) => parseClientRouteItem(child, currentParams, path)) || []];
99
99
  };
100
100
  var createRouter = (clientList) => clientList.flatMap((el) => parseClientRouteItem(el, []));
101
- var getParamsObject = (params) => {
102
- const { pathname } = window.location;
101
+ var getParamsObject = ({ routeItem, pathname }) => {
102
+ if (!routeItem?.params) return {};
103
103
  const split = pathname.split("/");
104
- return (params || []).map((el) => ({
104
+ return (routeItem.params || []).map((el) => ({
105
105
  index: split.findIndex((item) => item === el.key),
106
106
  value: el.value
107
107
  })).reduce((acc, cur) => ({
@@ -135,6 +135,7 @@ var useHandleNavigation = ({ setLocation, routeList, context, revalidateCache, i
135
135
  from: "",
136
136
  to: ""
137
137
  });
138
+ const [beforeLoadError, setBeforeLoadError] = useState(false);
138
139
  const prevPathname = useRef("");
139
140
  const navigationSeq = useRef(0);
140
141
  const navigation = useCallback((nextLocation) => {
@@ -151,31 +152,53 @@ var useHandleNavigation = ({ setLocation, routeList, context, revalidateCache, i
151
152
  }
152
153
  else navigation(nextLocation);
153
154
  }, [navigation]);
154
- const setNextLocation = useCallback(async (nextLocation, isFirstCall) => {
155
+ const navigationHandler = useCallback(async (nextLocation, isFirstCall) => {
155
156
  navigationSeq.current = navigationSeq.current + 1;
156
157
  const seq = navigationSeq.current;
157
158
  const nextItem = routeList.find((el) => comparePaths(el, nextLocation.pathname));
158
- if (nextItem?.beforeLoad) await nextItem.beforeLoad({
159
- context,
160
- redirect: setNextLocation
159
+ const params = getParamsObject({
160
+ routeItem: nextItem,
161
+ pathname: nextLocation.pathname
161
162
  });
163
+ if (nextItem?.beforeLoad) try {
164
+ await nextItem.beforeLoad({
165
+ context,
166
+ redirect: navigationHandler,
167
+ params
168
+ });
169
+ } catch {
170
+ setBeforeLoadError(true);
171
+ transitionedNavigation({
172
+ nextLocation,
173
+ isAnimated: false
174
+ });
175
+ return;
176
+ }
162
177
  if (seq !== navigationSeq.current) return;
163
- await revalidateCache(nextItem, true);
178
+ await revalidateCache({
179
+ routeItem: nextItem,
180
+ isCurrentRoute: true,
181
+ pathname: nextLocation.pathname
182
+ });
164
183
  if (seq !== navigationSeq.current) return;
165
184
  transitionedNavigation({
166
185
  nextLocation,
167
- isAnimated,
168
- isFirstCall
186
+ isFirstCall,
187
+ isAnimated
188
+ });
189
+ setBeforeLoadError(false);
190
+ if (nextItem?.afterLoad) await nextItem.afterLoad({
191
+ context,
192
+ params
169
193
  });
170
- if (nextItem?.afterLoad) await nextItem.afterLoad(context);
171
194
  }, [
172
195
  context,
173
- isAnimated,
174
196
  revalidateCache,
175
197
  routeList,
176
- transitionedNavigation
198
+ transitionedNavigation,
199
+ isAnimated
177
200
  ]);
178
- const setNextLocationRef = useLatest(setNextLocation);
201
+ const setNextLocationRef = useLatest(navigationHandler);
179
202
  const updateBlockedRoute = useCallback(({ type, payload = "" }) => setBlockedRoute((prevState) => {
180
203
  if (prevState.from === payload && type === "charge") return prevState;
181
204
  if (payload && prevState.from !== payload && type === "charge") return {
@@ -226,26 +249,33 @@ var useHandleNavigation = ({ setLocation, routeList, context, revalidateCache, i
226
249
  return "unblocked";
227
250
  }, [blockedRoute]),
228
251
  updateLocation,
229
- updateBlockedRoute
252
+ updateBlockedRoute,
253
+ beforeLoadError
230
254
  };
231
255
  };
232
256
  //#endregion
233
257
  //#region hooks/useLoader.ts
234
- var useLoader = (routeList) => {
258
+ var useLoader = ({ routeList, context }) => {
235
259
  const [loaderCache, setLoaderCache] = useState();
236
260
  const [loaderError, setLoaderError] = useState(false);
237
261
  const [isLoading, setIsLoading] = useState(false);
238
262
  const cacheTimestampsRef = useRef({});
239
263
  const loaderCacheRef = useRef({});
240
- const isCacheItemFresh = useCallback((routeItem) => {
264
+ const isCacheItemFresh = useCallback(({ routeItem, pathname }) => {
241
265
  if (!routeItem) return true;
242
- const currentCacheTimestamp = cacheTimestampsRef.current[routeItem.path];
266
+ const currentCacheTimestamp = cacheTimestampsRef.current[pathname];
243
267
  return Boolean(currentCacheTimestamp && Date.now() - currentCacheTimestamp < (routeItem.staleTime || 0));
244
268
  }, []);
245
- const revalidateCache = useCallback(async (routeItem, isCurrentRoute) => {
269
+ const revalidateCache = useCallback(async ({ routeItem, isCurrentRoute, pathname }) => {
246
270
  if (!routeItem?.loader) return;
247
- if (isCacheItemFresh(routeItem) && isCurrentRoute) setLoaderCache(loaderCacheRef.current[routeItem.path]);
248
- if (isCacheItemFresh(routeItem)) return;
271
+ if (isCacheItemFresh({
272
+ routeItem,
273
+ pathname
274
+ }) && isCurrentRoute) setLoaderCache(loaderCacheRef.current[pathname]);
275
+ if (isCacheItemFresh({
276
+ routeItem,
277
+ pathname
278
+ })) return;
249
279
  if (isCurrentRoute) setIsLoading(true);
250
280
  loaderCacheRef.current = Object.keys(loaderCacheRef.current).filter((el) => el !== routeItem.path).reduce((acc, cur) => ({
251
281
  ...acc,
@@ -253,14 +283,21 @@ var useLoader = (routeList) => {
253
283
  }), {});
254
284
  try {
255
285
  if (isCurrentRoute) setLoaderError(false);
256
- const result = await routeItem?.loader();
286
+ const params = getParamsObject({
287
+ routeItem,
288
+ pathname
289
+ });
290
+ const result = await routeItem?.loader({
291
+ params,
292
+ context
293
+ });
257
294
  cacheTimestampsRef.current = {
258
295
  ...cacheTimestampsRef.current,
259
- [routeItem.path]: Date.now()
296
+ [pathname]: Date.now()
260
297
  };
261
298
  loaderCacheRef.current = {
262
299
  ...loaderCacheRef.current,
263
- [routeItem.path]: result
300
+ [pathname]: result
264
301
  };
265
302
  if (isCurrentRoute) setLoaderCache(result);
266
303
  } catch {
@@ -268,13 +305,16 @@ var useLoader = (routeList) => {
268
305
  } finally {
269
306
  if (isCurrentRoute) setIsLoading(false);
270
307
  }
271
- }, [isCacheItemFresh]);
308
+ }, [context, isCacheItemFresh]);
272
309
  return {
273
310
  loaderCache,
274
311
  loaderError,
275
312
  prefetchLoader: useCallback(async (pathname) => {
276
313
  const item = routeList.find((el) => comparePaths(el, pathname));
277
- if (item) await revalidateCache(item);
314
+ if (item) await revalidateCache({
315
+ routeItem: item,
316
+ pathname
317
+ });
278
318
  }, [revalidateCache, routeList]),
279
319
  revalidateCache,
280
320
  isLoading
@@ -373,15 +413,21 @@ var Router = ({ routeList, context: initialContext = {}, isAnimated = false, ani
373
413
  const [context, setContext] = useState(initialContext);
374
414
  useApplyCustomAnimation(animationOptions);
375
415
  const routeItem = useMemo(() => routeList.find((el) => el.path === ALL_LOCATIONS || comparePaths(el, location.pathname)), [location.pathname, routeList]);
376
- const { loaderError, loaderCache, prefetchLoader, revalidateCache, isLoading } = useLoader(routeList);
377
- const { blockerState, updateLocation, updateBlockedRoute } = useHandleNavigation({
416
+ const params = useMemo(() => getParamsObject({
417
+ routeItem,
418
+ pathname: window.location.pathname
419
+ }), [routeItem]);
420
+ const { loaderError, loaderCache, prefetchLoader, revalidateCache, isLoading } = useLoader({
421
+ routeList,
422
+ context
423
+ });
424
+ const { blockerState, updateLocation, updateBlockedRoute, beforeLoadError } = useHandleNavigation({
378
425
  setLocation,
379
426
  routeList,
380
427
  context,
381
428
  revalidateCache,
382
429
  isAnimated
383
430
  });
384
- const params = useMemo(() => routeItem?.params ? getParamsObject(routeItem.params) : {}, [routeItem]);
385
431
  const providerProps = useMemo(() => ({
386
432
  location,
387
433
  updateLocation,
@@ -406,9 +452,9 @@ var Router = ({ routeList, context: initialContext = {}, isAnimated = false, ani
406
452
  ...providerProps,
407
453
  children: renderElement(routeItem?.loaderFallback)
408
454
  });
409
- if (loaderError) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RouterProvider, {
455
+ if (loaderError || beforeLoadError) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(RouterProvider, {
410
456
  ...providerProps,
411
- children: renderElement(routeItem?.errorElement)
457
+ children: [renderElement(routeItem?.errorElement), isAnimated && isLoading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {})]
412
458
  });
413
459
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(RouterProvider, {
414
460
  ...providerProps,
@@ -5,7 +5,10 @@ export type LazyComponent = () => Promise<{
5
5
  export type ClientRouteItem = {
6
6
  path: string;
7
7
  element: (() => ReactElement) | ReactElement | LazyComponent;
8
- loader?(...args: unknown[]): Promise<unknown>;
8
+ loader?(arg: {
9
+ params: Record<string, string>;
10
+ context: Record<string, unknown>;
11
+ }): Promise<unknown>;
9
12
  loaderFallback?: (() => ReactElement) | ReactElement;
10
13
  errorElement?: (() => ReactElement) | ReactElement;
11
14
  fallback?: (() => ReactElement) | ReactElement;
@@ -14,8 +17,12 @@ export type ClientRouteItem = {
14
17
  beforeLoad?: (arg: {
15
18
  context: Record<string, unknown>;
16
19
  redirect: (arg: Location) => Promise<void>;
20
+ params: Record<string, string>;
17
21
  }) => Promise<unknown> | undefined;
18
- afterLoad?: (context: Record<string, unknown>) => Promise<unknown> | undefined;
22
+ afterLoad?: (arg: {
23
+ context: Record<string, unknown>;
24
+ params: Record<string, string>;
25
+ }) => Promise<void>;
19
26
  };
20
27
  export type RouteItem = ClientRouteItem & {
21
28
  element: (() => ReactElement) | ReactElement;
@@ -39,3 +46,8 @@ export type AnimationOptions = {
39
46
  duration?: number;
40
47
  name?: 'fade' | 'slide-left' | 'slide-right';
41
48
  };
49
+ export type RevalidateCacheArgs = {
50
+ pathname: string;
51
+ routeItem?: RouteItem;
52
+ isCurrentRoute?: boolean;
53
+ };
@@ -1,5 +1,8 @@
1
- import type { ClientRouteItem, Location, RouteItem } from '../types/global.ts';
1
+ import type { ClientRouteItem, Location, RouteItem } from '../types/global';
2
2
  export declare const createRouter: (clientList: ClientRouteItem[]) => RouteItem[];
3
- export declare const getParamsObject: (params: RouteItem["params"]) => {};
3
+ export declare const getParamsObject: ({ routeItem, pathname }: {
4
+ routeItem?: RouteItem;
5
+ pathname: string;
6
+ }) => {};
4
7
  export declare const parseWindowLocation: (location: typeof window.location) => Location;
5
8
  export declare const comparePaths: (el: RouteItem, pathname: string) => boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clear-react-router",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
4
4
  "description": "A lightweight, type-safe routing library for React applications",
5
5
  "author": "Andrew Bubnov",
6
6
  "scripts": {