@ovineko/react-router 0.0.4 → 0.1.0
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 +88 -0
- package/dist/GuardedRoute.d.ts +9 -0
- package/dist/GuardedRoute.js +6 -0
- package/dist/chunk-TFMOCB4F.js +17 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +5 -0
- package/dist/types.d.ts +8 -0
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -250,6 +250,94 @@ function UserProfile() {
|
|
|
250
250
|
}
|
|
251
251
|
```
|
|
252
252
|
|
|
253
|
+
#### `GuardedRoute`
|
|
254
|
+
|
|
255
|
+
Declarative route guard wrapper for React Router v7 route configs. Makes route protection visible at the routing config level instead of hidden inside page components.
|
|
256
|
+
|
|
257
|
+
**Props:**
|
|
258
|
+
|
|
259
|
+
- `useGuard: () => GuardResult` - A React hook that returns guard state
|
|
260
|
+
- `loadingFallback?: React.ReactNode` - Optional loading UI (blocks child rendering)
|
|
261
|
+
|
|
262
|
+
**GuardResult interface:**
|
|
263
|
+
|
|
264
|
+
```tsx
|
|
265
|
+
interface GuardResult {
|
|
266
|
+
allowed: boolean; // Whether the user is allowed to access this route
|
|
267
|
+
isLoading: boolean; // Whether the guard data is still loading
|
|
268
|
+
redirectTo: string; // Where to redirect when !allowed && !isLoading
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
**Behavior:**
|
|
273
|
+
|
|
274
|
+
- **Default (no `loadingFallback`):** Renders `<Outlet />` immediately while `isLoading` is true. This allows the child page's lazy import to start in parallel with the guard's data fetching.
|
|
275
|
+
- **With `loadingFallback`:** Renders the fallback instead of `<Outlet />` while `isLoading` is true. Use this when you need to block child rendering until the guard resolves.
|
|
276
|
+
- **Redirect:** When `isLoading` is false and `allowed` is false, redirects to `redirectTo` using `replace` (guards are access checks, not navigation).
|
|
277
|
+
|
|
278
|
+
**Example - Extracting Guard from Data Hook:**
|
|
279
|
+
|
|
280
|
+
A page has a `useOrderData` hook that fetches data. Without a guard, a user can navigate directly to `/orders/123/checkout` even when the order doesn't exist — the redirect logic is buried inside the data hook and invisible from the route config.
|
|
281
|
+
|
|
282
|
+
```tsx
|
|
283
|
+
import { GuardedRoute } from "@ovineko/react-router";
|
|
284
|
+
import type { GuardResult } from "@ovineko/react-router";
|
|
285
|
+
|
|
286
|
+
// ✅ Guard hook — extracted, visible in route config
|
|
287
|
+
function useOrderGuard(): GuardResult {
|
|
288
|
+
const { orderId } = useParams<{ orderId: string }>();
|
|
289
|
+
const { data, isLoading } = useSWR(`/api/orders/${orderId}`);
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
allowed: Boolean(data?.order),
|
|
293
|
+
isLoading,
|
|
294
|
+
redirectTo: "/orders",
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ✅ Data hook — pure, no redirects
|
|
299
|
+
function useOrderData() {
|
|
300
|
+
const { orderId } = useParams<{ orderId: string }>();
|
|
301
|
+
const { data, isLoading } = useSWR(`/api/orders/${orderId}`);
|
|
302
|
+
return { order: data?.order, isLoading };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function Checkout() {
|
|
306
|
+
const { order, isLoading } = useOrderData();
|
|
307
|
+
// No guard logic here — GuardedRoute already handled it
|
|
308
|
+
if (isLoading) return <Spinner />;
|
|
309
|
+
return <div>Checkout for order {order.id}</div>;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ✅ Route config — protection is visible and declarative
|
|
313
|
+
const routes: RouteObject[] = [
|
|
314
|
+
{
|
|
315
|
+
element: <GuardedRoute useGuard={useOrderGuard} />,
|
|
316
|
+
children: [{ path: "orders/:orderId/checkout", element: <Checkout /> }],
|
|
317
|
+
},
|
|
318
|
+
];
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
**Example - With `loadingFallback`:**
|
|
322
|
+
|
|
323
|
+
When you need to block child rendering until the guard resolves:
|
|
324
|
+
|
|
325
|
+
```tsx
|
|
326
|
+
const protectedRoutes: RouteObject[] = [
|
|
327
|
+
{
|
|
328
|
+
element: <GuardedRoute useGuard={useAuthGuard} loadingFallback={<Spinner />} />,
|
|
329
|
+
children: [{ path: "dashboard", element: <Dashboard /> }],
|
|
330
|
+
},
|
|
331
|
+
];
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
**Comparison with `useRedirect`:**
|
|
335
|
+
|
|
336
|
+
- `useRedirect` - Hook-level conditional redirect (used inside components)
|
|
337
|
+
- `GuardedRoute` - Config-level declarative guard (used in route definitions)
|
|
338
|
+
|
|
339
|
+
Both complement each other: `GuardedRoute` makes route protection explicit in the config, while `useRedirect` handles component-level conditional navigation.
|
|
340
|
+
|
|
253
341
|
#### `optionalSearchParams(entries)`
|
|
254
342
|
|
|
255
343
|
Utility to make all search param fields optional automatically, avoiding repetitive `v.optional()` calls.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { GuardResult } from "./types";
|
|
2
|
+
export interface GuardedRouteProps {
|
|
3
|
+
/** Optional loading fallback shown while isLoading is true.
|
|
4
|
+
* When omitted, renders <Outlet /> immediately (allows parallel lazy loading). */
|
|
5
|
+
loadingFallback?: React.ReactNode;
|
|
6
|
+
/** A React hook that returns GuardResult. Called inside the component. */
|
|
7
|
+
useGuard: () => GuardResult;
|
|
8
|
+
}
|
|
9
|
+
export declare function GuardedRoute({ loadingFallback, useGuard }: GuardedRouteProps): string | number | bigint | true | Iterable<import("react").ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<import("react").ReactNode> | null | undefined> | import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// src/GuardedRoute.tsx
|
|
2
|
+
import { Navigate, Outlet } from "react-router";
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
function GuardedRoute({ loadingFallback, useGuard }) {
|
|
5
|
+
const { allowed, isLoading, redirectTo } = useGuard();
|
|
6
|
+
if (!isLoading && !allowed) {
|
|
7
|
+
return /* @__PURE__ */ jsx(Navigate, { replace: true, to: redirectTo });
|
|
8
|
+
}
|
|
9
|
+
if (isLoading && loadingFallback) {
|
|
10
|
+
return loadingFallback;
|
|
11
|
+
}
|
|
12
|
+
return /* @__PURE__ */ jsx(Outlet, {});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
GuardedRoute
|
|
17
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as v from "valibot";
|
|
2
2
|
import type { InferSearchParams, RouteWithoutParams, RouteWithParams, State } from "./types";
|
|
3
|
-
export
|
|
3
|
+
export { GuardedRoute } from "./GuardedRoute";
|
|
4
|
+
export type { GuardedRouteProps } from "./GuardedRoute";
|
|
5
|
+
export type { GuardResult, RouteBase, RouteWithoutParams, RouteWithParams, State, UseParamsRawResult, UseSearchParamsRawResult, } from "./types";
|
|
4
6
|
export { optionalSearchParams } from "./utils";
|
|
5
7
|
export { URLParseError } from "./validation";
|
|
6
8
|
export declare const setGlobalErrorRedirect: (url: string) => void;
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
GuardedRoute
|
|
3
|
+
} from "./chunk-TFMOCB4F.js";
|
|
4
|
+
|
|
1
5
|
// src/index.tsx
|
|
2
6
|
import { memo, useCallback, useEffect, useMemo, useRef } from "react";
|
|
3
7
|
import {
|
|
@@ -306,6 +310,7 @@ var replaceState = (state) => {
|
|
|
306
310
|
);
|
|
307
311
|
};
|
|
308
312
|
export {
|
|
313
|
+
GuardedRoute,
|
|
309
314
|
URLParseError,
|
|
310
315
|
createRouteWithParams,
|
|
311
316
|
createRouteWithoutParams,
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import type { LinkProps as LinkPropsLib, NavigateOptions } from "react-router";
|
|
2
2
|
import type * as v from "valibot";
|
|
3
|
+
export interface GuardResult {
|
|
4
|
+
/** Whether the user is allowed to access this route */
|
|
5
|
+
allowed: boolean;
|
|
6
|
+
/** Whether the guard data is still loading */
|
|
7
|
+
isLoading: boolean;
|
|
8
|
+
/** Where to redirect when !allowed && !isLoading */
|
|
9
|
+
redirectTo: string;
|
|
10
|
+
}
|
|
3
11
|
export type InferSearchParams<T> = T extends v.GenericSchema ? v.InferOutput<T> : undefined;
|
|
4
12
|
export interface LinkPropsWithoutParams<SearchParams> extends Omit<LinkPropsLib, "to"> {
|
|
5
13
|
children: React.ReactNode | string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ovineko/react-router",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Type-safe wrapper for React Router v7 with typed params, query params, and Link components",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
},
|
|
27
27
|
"license": "MIT",
|
|
28
28
|
"author": "Alexander Svinarev <shibanet0@gmail.com> (shibanet0.com)",
|
|
29
|
+
"sideEffects": false,
|
|
29
30
|
"type": "module",
|
|
30
31
|
"exports": {
|
|
31
32
|
".": {
|
|
@@ -43,9 +44,9 @@
|
|
|
43
44
|
"valibot": "^1"
|
|
44
45
|
},
|
|
45
46
|
"engines": {
|
|
46
|
-
"node": ">=
|
|
47
|
+
"node": ">=22"
|
|
47
48
|
},
|
|
48
49
|
"publishConfig": {
|
|
49
50
|
"access": "public"
|
|
50
51
|
}
|
|
51
|
-
}
|
|
52
|
+
}
|