@ovineko/react-router
v0.1.0
Published
Type-safe wrapper for React Router v7 with typed params, query params, and Link components
Downloads
491
Maintainers
Readme
@ovineko/react-router
Type-safe wrapper for React Router v7 with valibot schema validation, automatic error handling, and typed params.
Install
pnpm add @ovineko/react-router valibotPeer dependencies: react@^19, react-router@^7, valibot@^1.
Features
- ✅ Valibot schema validation - Runtime validation for URL params and search params
- ✅ Two hook variants - Normal hooks (auto-redirect on error) and Raw hooks (manual error handling)
- ✅ Smart validation - Ignores extra query params and invalid optional params
- ✅ Global error handling - Configure fallback redirect URLs globally or per-route
- ✅ Conditional redirects -
useRedirecthook for declarative navigation with infinite loop prevention - ✅ Optional search params helper -
optionalSearchParamsutility to avoid repetitivev.optional()calls - ✅ Type-safe - Full TypeScript support with inferred types
- ✅ Hash support - Generate paths with hash fragments
Quick Start
import { createRouteWithParams, optionalSearchParams } from "@ovineko/react-router";
import * as v from "valibot";
const userRoute = createRouteWithParams("/users/:id", {
params: v.object({ id: v.pipe(v.string(), v.uuid()) }),
searchParams: optionalSearchParams({
tab: v.string(),
page: v.pipe(v.string(), v.transform(Number), v.number()),
}),
errorRedirect: "/404", // Optional: redirect on validation error
});
// Normal hooks - auto-redirect on validation error
function UserPage() {
const params = userRoute.useParams(); // Readonly<{ id: string }>
const [searchParams, setSearchParams] = userRoute.useSearchParams();
return (
<div>
User {params.id}, Page {searchParams.page ?? 1}
</div>
);
}
// Raw hooks - manual error handling
function AdvancedUserPage() {
const [params, error] = userRoute.useParamsRaw();
const { data, error: searchError } = userRoute.useSearchParamsRaw();
if (error) return <div>Invalid user ID</div>;
if (searchError) console.warn("Invalid search params", searchError);
return <div>User {params?.id}</div>;
}Global Error Redirect
Configure a global fallback URL for validation errors:
import { setGlobalErrorRedirect } from "@ovineko/react-router";
// In your app entry point
setGlobalErrorRedirect("/error");
// Priority: route-level > global > default "/"
const route = createRouteWithParams("/users/:id", {
params: v.object({ id: v.string() }),
errorRedirect: "/404", // Overrides global
});Validation Behavior
Path Params
- Always strict validation
- Redirect on any validation error
Search Params
- Extra params (not in schema) → ignored
- Invalid optional params → ignored (treated as undefined)
- Invalid required params → error (triggers redirect)
const route = createRouteWithParams("/search", {
params: v.object({}),
searchParams: v.object({
q: v.string(), // required
page: v.optional(v.pipe(v.string(), v.transform(Number), v.number())),
}),
});
// URL: /search?q=react&page=invalid&debug=true
// Result: { q: "react", page: undefined }
// - page=invalid ignored (optional + invalid)
// - debug=true ignored (not in schema)API Reference
Route Creation
createRouteWithParams
const route = createRouteWithParams<TParams, TSearchParams>(pattern, config);Config:
params- Valibot schema for URL params (required)searchParams?- Valibot schema for search params (optional)errorRedirect?- Redirect URL on validation error (optional)
Returns:
path(params, searchParams?, hash?)- Generate URL pathparseURLParams(url)- Parse and validate URL paramsLink- Type-safe Link componentuseParams()- Get validated params (auto-redirect on error)useParamsRaw()- Get params with error info[data, error]useSearchParams()- Get validated search params (auto-redirect on error)useSearchParamsRaw()- Get search params with error info{data, error, setter}pattern- Original route pattern
createRouteWithoutParams
const route = createRouteWithoutParams<TSearchParams>(pattern, config?);Config:
searchParams?- Valibot schema for search paramserrorRedirect?- Redirect URL on validation error
Returns:
path(searchParams?, hash?)- Generate URL pathLink- Type-safe Link componentuseSearchParams()- Get validated search paramsuseSearchParamsRaw()- Get search params with error infopattern- Original route pattern
Examples
Path Generation
const userRoute = createRouteWithParams("/users/:id", {
params: v.object({ id: v.string() }),
searchParams: v.object({
tab: v.optional(v.string()),
}),
});
userRoute.path({ id: "42" });
// "/users/42"
userRoute.path({ id: "42" }, { tab: "settings" });
// "/users/42?tab=settings"
userRoute.path({ id: "42" }, { tab: "settings" }, "profile");
// "/users/42?tab=settings#profile"Type-safe Links
<userRoute.Link params={{ id: "42" }} searchParams={{ tab: "profile" }}>
View Profile
</userRoute.Link>
// Automatically includes prevPath in navigation stateParse URL Params
const params = userRoute.parseURLParams("https://example.com/users/42");
// { id: "42" }
// Throws URLParseError on invalid URL or validation errorUpdate Search Params
const [searchParams, setSearchParams] = route.useSearchParams();
// Set new params
setSearchParams({ q: "react", page: 1 });
// Update based on previous
setSearchParams((prev) => ({ ...prev, page: prev.page + 1 }));
// With navigation options
setSearchParams({ q: "vue" }, { replace: true });Utilities
useRedirect(path, condition, replace?)
Declarative hook for conditional redirects with built-in infinite loop prevention.
Parameters:
path: string- Target redirect URLcondition: boolean- Whether to trigger the redirectreplace?: boolean- Use replace instead of push (default:true)
Features:
- Prevents infinite redirect loops using
useReftracking - Only redirects once when condition becomes
true - Resets automatically when condition becomes
false - Uses
replace: trueby default to prevent back-button issues
import { useRedirect } from "@ovineko/react-router";
function ProtectedPage() {
const { isAuthenticated, isLoading } = useAuth();
// Redirect to login if not authenticated
useRedirect("/login", !isAuthenticated && !isLoading);
if (isLoading) return <Spinner />;
return <div>Protected Content</div>;
}
// Advanced usage with custom options
function UserProfile() {
const { user, error } = useUser();
// Redirect without replacing history
useRedirect("/users", !user && !error, false);
return <div>{user?.name}</div>;
}GuardedRoute
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.
Props:
useGuard: () => GuardResult- A React hook that returns guard stateloadingFallback?: React.ReactNode- Optional loading UI (blocks child rendering)
GuardResult interface:
interface GuardResult {
allowed: boolean; // Whether the user is allowed to access this route
isLoading: boolean; // Whether the guard data is still loading
redirectTo: string; // Where to redirect when !allowed && !isLoading
}Behavior:
- Default (no
loadingFallback): Renders<Outlet />immediately whileisLoadingis true. This allows the child page's lazy import to start in parallel with the guard's data fetching. - With
loadingFallback: Renders the fallback instead of<Outlet />whileisLoadingis true. Use this when you need to block child rendering until the guard resolves. - Redirect: When
isLoadingis false andallowedis false, redirects toredirectTousingreplace(guards are access checks, not navigation).
Example - Extracting Guard from Data Hook:
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.
import { GuardedRoute } from "@ovineko/react-router";
import type { GuardResult } from "@ovineko/react-router";
// ✅ Guard hook — extracted, visible in route config
function useOrderGuard(): GuardResult {
const { orderId } = useParams<{ orderId: string }>();
const { data, isLoading } = useSWR(`/api/orders/${orderId}`);
return {
allowed: Boolean(data?.order),
isLoading,
redirectTo: "/orders",
};
}
// ✅ Data hook — pure, no redirects
function useOrderData() {
const { orderId } = useParams<{ orderId: string }>();
const { data, isLoading } = useSWR(`/api/orders/${orderId}`);
return { order: data?.order, isLoading };
}
function Checkout() {
const { order, isLoading } = useOrderData();
// No guard logic here — GuardedRoute already handled it
if (isLoading) return <Spinner />;
return <div>Checkout for order {order.id}</div>;
}
// ✅ Route config — protection is visible and declarative
const routes: RouteObject[] = [
{
element: <GuardedRoute useGuard={useOrderGuard} />,
children: [{ path: "orders/:orderId/checkout", element: <Checkout /> }],
},
];Example - With loadingFallback:
When you need to block child rendering until the guard resolves:
const protectedRoutes: RouteObject[] = [
{
element: <GuardedRoute useGuard={useAuthGuard} loadingFallback={<Spinner />} />,
children: [{ path: "dashboard", element: <Dashboard /> }],
},
];Comparison with useRedirect:
useRedirect- Hook-level conditional redirect (used inside components)GuardedRoute- Config-level declarative guard (used in route definitions)
Both complement each other: GuardedRoute makes route protection explicit in the config, while useRedirect handles component-level conditional navigation.
optionalSearchParams(entries)
Utility to make all search param fields optional automatically, avoiding repetitive v.optional() calls.
Parameters:
entries: ObjectEntries- Valibot schema entries for search params
Returns: Valibot object schema with all fields wrapped in v.optional()
import { optionalSearchParams } from "@ovineko/react-router";
import * as v from "valibot";
// ❌ Before: Manual v.optional() for each field
const route = createRouteWithParams("/search", {
params: v.object({ id: v.string() }),
searchParams: v.object({
q: v.optional(v.string()),
page: v.optional(v.pipe(v.string(), v.transform(Number), v.number())),
sort: v.optional(v.string()),
filter: v.optional(v.string()),
}),
});
// ✅ After: Clean and concise
const route = createRouteWithParams("/search", {
params: v.object({ id: v.string() }),
searchParams: optionalSearchParams({
q: v.string(),
page: v.pipe(v.string(), v.transform(Number), v.number()),
sort: v.string(),
filter: v.string(),
}),
});
// All fields are automatically optional!
const [searchParams] = route.useSearchParams();
// Type: Readonly<{ q?: string; page?: number; sort?: string; filter?: string }>Benefits:
- Cleaner, more readable code
- Less boilerplate for search params (which are typically optional)
- Full TypeScript support with proper type inference
- Works with any Valibot schema (transformations, pipes, etc.)
setGlobalErrorRedirect(url)
Set global fallback redirect URL for validation errors.
setGlobalErrorRedirect("/error");replaceState(state)
Update browser history state without navigation.
import { replaceState } from "@ovineko/react-router";
replaceState({ scrollTop: window.scrollY });URLParseError
Error class thrown when URL parsing fails.
try {
route.parseURLParams("invalid-url");
} catch (error) {
if (error instanceof URLParseError) {
console.log(error.context); // { pattern, url, ... }
}
}TypeScript
All types are automatically inferred from valibot schemas:
const route = createRouteWithParams("/users/:id", {
params: v.object({ id: v.string() }),
searchParams: v.object({
page: v.optional(v.pipe(v.string(), v.transform(Number), v.number())),
}),
});
// Inferred types:
const params = route.useParams(); // Readonly<{ id: string }>
const [searchParams] = route.useSearchParams(); // Readonly<{ page?: number }>Benefits:
- Runtime validation
- Automatic error handling
- Schema transformations (e.g., string → number)
- Better type inference
License
MIT
