@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 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,6 @@
1
+ import {
2
+ GuardedRoute
3
+ } from "./chunk-TFMOCB4F.js";
4
+ export {
5
+ GuardedRoute
6
+ };
@@ -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 type { RouteBase, RouteWithoutParams, RouteWithParams, State, UseParamsRawResult, UseSearchParamsRawResult, } from "./types";
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.4",
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": ">=20"
47
+ "node": ">=22"
47
48
  },
48
49
  "publishConfig": {
49
50
  "access": "public"
50
51
  }
51
- }
52
+ }