clear-react-router 1.2.2 → 1.2.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
@@ -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
@@ -5,5 +5,5 @@ type RouterProps = {
5
5
  isAnimated?: boolean;
6
6
  animationOptions?: AnimationOptions;
7
7
  };
8
- export declare const Router: ({ routeList, context: initialContext, isAnimated, animationOptions, }: RouterProps) => import("react/jsx-runtime").JSX.Element;
8
+ export declare const Router: ({ routeList, context: initialContext, isAnimated, animationOptions, }: RouterProps) => import("react/jsx-runtime").JSX.Element | null;
9
9
  export {};
@@ -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
@@ -369,19 +409,28 @@ var renderElement = (Component) => {
369
409
  var PAGE_NOT_FOUND = "error 404. Page not found";
370
410
  var ALL_LOCATIONS = "*";
371
411
  var Router = ({ routeList, context: initialContext = {}, isAnimated = false, animationOptions = {} }) => {
372
- const [location, setLocation] = useState(parseWindowLocation(window.location));
412
+ const [location, setLocation] = useState({});
373
413
  const [context, setContext] = useState(initialContext);
374
414
  useApplyCustomAnimation(animationOptions);
375
- 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({
415
+ const routeItem = useMemo(() => {
416
+ if (!location.pathname) return void 0;
417
+ return routeList.find((el) => el.path === ALL_LOCATIONS || comparePaths(el, location.pathname));
418
+ }, [location.pathname, routeList]);
419
+ const params = useMemo(() => getParamsObject({
420
+ routeItem,
421
+ pathname: window.location.pathname
422
+ }), [routeItem]);
423
+ const { loaderError, loaderCache, prefetchLoader, revalidateCache, isLoading } = useLoader({
424
+ routeList,
425
+ context
426
+ });
427
+ const { blockerState, updateLocation, updateBlockedRoute, beforeLoadError } = useHandleNavigation({
378
428
  setLocation,
379
429
  routeList,
380
430
  context,
381
431
  revalidateCache,
382
432
  isAnimated
383
433
  });
384
- const params = useMemo(() => routeItem?.params ? getParamsObject(routeItem.params) : {}, [routeItem]);
385
434
  const providerProps = useMemo(() => ({
386
435
  location,
387
436
  updateLocation,
@@ -402,13 +451,14 @@ var Router = ({ routeList, context: initialContext = {}, isAnimated = false, ani
402
451
  updateBlockedRoute,
403
452
  updateLocation
404
453
  ]);
454
+ if (!routeItem) return null;
405
455
  if (!isAnimated && routeItem?.loader && !loaderError && isLoading) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RouterProvider, {
406
456
  ...providerProps,
407
457
  children: renderElement(routeItem?.loaderFallback)
408
458
  });
409
- if (loaderError) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RouterProvider, {
459
+ if (loaderError || beforeLoadError) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(RouterProvider, {
410
460
  ...providerProps,
411
- children: renderElement(routeItem?.errorElement)
461
+ children: [renderElement(routeItem?.errorElement), isAnimated && isLoading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {})]
412
462
  });
413
463
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(RouterProvider, {
414
464
  ...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.4",
4
4
  "description": "A lightweight, type-safe routing library for React applications",
5
5
  "author": "Andrew Bubnov",
6
6
  "scripts": {