npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ovineko/react-router

v0.1.0

Published

Type-safe wrapper for React Router v7 with typed params, query params, and Link components

Downloads

491

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 valibot

Peer 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 - useRedirect hook for declarative navigation with infinite loop prevention
  • Optional search params helper - optionalSearchParams utility to avoid repetitive v.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 path
  • parseURLParams(url) - Parse and validate URL params
  • Link - Type-safe Link component
  • useParams() - 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 params
  • errorRedirect? - Redirect URL on validation error

Returns:

  • path(searchParams?, hash?) - Generate URL path
  • Link - Type-safe Link component
  • useSearchParams() - Get validated search params
  • useSearchParamsRaw() - Get search params with error info
  • pattern - 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 state

Parse URL Params

const params = userRoute.parseURLParams("https://example.com/users/42");
// { id: "42" }

// Throws URLParseError on invalid URL or validation error

Update 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 URL
  • condition: boolean - Whether to trigger the redirect
  • replace?: boolean - Use replace instead of push (default: true)

Features:

  • Prevents infinite redirect loops using useRef tracking
  • Only redirects once when condition becomes true
  • Resets automatically when condition becomes false
  • Uses replace: true by 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 state
  • loadingFallback?: 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 while isLoading is 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 /> while isLoading is true. Use this when you need to block child rendering until the guard resolves.
  • Redirect: When isLoading is false and allowed is false, redirects to redirectTo using replace (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