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

routexiz

v0.2.4

Published

A modern tree-based router for React with intent-driven navigation, Suspense-first data loading, and built-in guards, middleware, and caching.

Readme

🕹️ routexiz

NPM Downloads Bundle Size

LIVE EXAMPLE

A lightweight, modern, and flexible React router with tree-based routing, Suspense-first data loading, and nested route support.

Routes are modeled like a tree, where each route is a node and navigation resolves a path from root to leaf. Nested layouts, guards, middlewares, and per-route hooks are fully supported.


Why routexiz?

Traditional routing:

navigate("/users/1")

routexiz:

navigate("/users/:id", {
  params: { id: 1 }
})
  • ⚡ Suspense data loading (like React 18 philosophy)
  • 🧠 Guards (before navigation)
  • 🔧 Middleware (side-effects)
  • 🔁 Nested routing builder API
  • 🚀 Prefetch (hover + viewport)
  • 🧩 Loader caching with TTL
  • ❌ Error boundary per route
  • 🎨 Transition support

Installation

npm install routexiz

Basic

import { route, RouterProvider } from "routexiz"

route("/", () => <div>Home</div>)

export default function App() {
  return <RouterProvider />
}

Nested Routing & Builder API

route("/", Layout, root => {
  root.route("/dashboard", Dashboard, dash => {
    dash.route("/stats", () => <div>Dashboard Stats Page</div>);
    dash.route("/settings", () => <div>Dashboard Settings Page</div>);
  });

  root.route("/users", Users); 
  root.route("/users/:id", User, {
    loader: async ({ params }) => ({ id: params.id, name: "User " + params.id }),
    fallback: <div>Loading user...</div>,
    errorBoundary: ({ error }) => <div>Error: {String(error)}</div>,
  });

  root.route("*", NotFound); // wildcard fallback
});

Each node can have its own layout, children, loader, hooks, and options.


Navigation

useNavigate / navigate

const navigate = useNavigate()

navigate("/users/:id", {
  params: { id: 1 },
  query: { tab: "profile" }

  // transition: 'fade',
  // duration: 100
})

redirect

import { redirect } from "routexiz";

redirect("/users/:id", { params: { id: 1 } });

Link

<Link to="/users/:id" params={{ id: 1 }} query={{ tab: "profile" }}>
  User 1
</Link>

<NavLink
  to="/users/:id"
  params={{ id: 1 }}
  className={({ isActive }) => isActive ? "active" : "link"}
>
  User 1
</NavLink>

Data Loading

Loader allows async data fetching per route, compatible with React Suspense:

root.route("/users/:id", User, {
  loader: async ({ params }) => fetchUser(params.id),
  fallback: <div>Loading user...</div>,
  errorBoundary: ({ error }) => <div>Error: {String(error)}</div>
});

Use useLoaderData() to access loaded data inside components

// Access loader data:
const data = useLoaderData()

Route Options & Hooks

1️⃣ Guard

A guard determines whether navigation is allowed.

  • Runs before entering the route
  • Can block navigation
root.route("/dashboard", Dashboard, dash => {
  dash.guard(({ params }) => {
    if (!isLoggedIn()) return false; // block
    return true; // allow
  });
});

2️⃣ Middleware

Middleware runs after guards, used for side effects.

  • Cannot block navigation
  • Can be async
dash.middleware(async ({ params, query }) => {
  console.log("Visited dashboard with params:", params);
});

3️⃣ onEnter / onLeave

Hooks triggered when entering or leaving a route node:

root.onEnter(({ path, params }) => console.log("Enter root:", path));
root.onLeave(({ path, params }) => console.log("Leave root:", path));
  • onEnter → called when node becomes active
  • onLeave → called when node is left

Guards - Middleware

| Creteria | Guard | Middleware | | --------------- | ------------------------- | -------------------- | | Purpose | Control access | Perform side effects | | Can block route | ✅ Yes | ❌ No | | Return value | true / false / redirect | Ignored | | Execution order | First | After guards | | Async support | ✅ Yes | ✅ Yes |

Mental Model

  • Guard = "Can we enter?"
  • Middleware = "Do something while entering"

4️⃣ prefetch

Prefetch loader data in advance:

<Link to="/users/:id" params={{ id: 1 }}>User 1</Link>
  • Hover → prefetch by default
  • Viewport → prefetch when enters viewport
  • Disable: disablePrefetch

5️⃣ errorBoundary / fallback

root.route("/users/:id", User, {
  fallback: <div>Loading user...</div>,
  errorBoundary: ({ error }) => <div>Error: {String(error)}</div>,
});

Lazy

import React, { Suspense } from "react"
import { route, RouterProvider } from "routexiz"

// Lazy load page
const Dashboard = React.lazy(() => import("./pages/Dashboard"))

route("/", Dashboard, {
  fallback: <div>Loading Dashboard...</div>,
})

Cache (TTL)

1️⃣ Declare route with cache

route("/users/:id", User, {
  loader: async ({ params }) => {
    const res = await fetch(`https://jsonplaceholder.typicode.com/users/${params.id}`)
    if (!res.ok) throw new Error("User not found")
    return res.json()
  },
  ttl: 5000, // cache expires in 5 seconds
  fallback: <div>Loading user...</div>,
  errorBoundary: ({ error }) => <div>Error: {String(error)}</div>
})

2️⃣ Access cached data inside component

import { useLoaderData, useParams } from "routexiz"

function User() {
  const data = useLoaderData<any>()
  const params = useParams()

  return (
    <div>
      User {params.id}
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  )
}

3️⃣ Prefetch / warm up cache

import { prefetch } from "routexiz"

// Prefetch user 1 in background
prefetch("/users/1")

4️⃣ Cleanup expired cache

import { cleanupCache } from "routexiz"

// Remove all expired resources
cleanupCache()

// Remove only 10 oldest expired items
cleanupCache(10)

TTL cache ensures route loaders are cached for fast navigation and automatically cleaned after expiry.


Global Fallback & Error

routexiz allows you to configure global fallback UI and global error boundaries for your entire app.

1️⃣ Set Global Fallback

import { setGlobalFallback } from "routexiz"

// Enable global fallback with a custom loader
setGlobalFallback(true, <div>App is loading...</div>)
  • If a route does not provide its own fallback, this global fallback will be used.

  • Works with Suspense loaders.

2️⃣ Set Global Error

import { setGlobalError } from "routexiz"

// Enable global error handling with a custom render
setGlobalError(true, (error) => (
  <div style={{ color: "red" }}>Oops! {String(error)}</div>
))
  • If a route does not provide its own errorBoundary, this global error component will be used.

  • Receives an error object and must return a valid React element.

3️⃣ Example with RouterProvider

import React, { Suspense } from "react"
import { RouterProvider, setGlobalFallback, setGlobalError } from "routexiz"

setGlobalFallback(true, <div>Loading app...</div>)
setGlobalError(true, (error) => <div>Error occurred: {String(error)}</div>)

export default function App() {
  return (
    <Suspense fallback={null}>
      <RouterProvider />
    </Suspense>
  )
}
  • The Suspense wrapper can still override the global fallback if needed.

  • Per-route fallback and errorBoundary take priority over global settings.


Hooks

1️⃣ useLoaderData()

Returns the data loaded by the route's loader. Suspense-aware.

import { useLoaderData } from "routexiz"

function User() {
  const data = useLoaderData<{ id: string; name: string }>()
  return (
    <div>
      User {data.id}: {data.name}
    </div>
  )
}

2️⃣ useParams()

Returns the dynamic route parameters.

import { useParams } from "routexiz"

function User() {
  const params = useParams()
  return <div>User ID: {params.id}</div>
}

3️⃣ useRouteContext()

Returns the current route path.

import { useRouteContext } from "routexiz"

function CurrentRoute() {
  const path = useRouteContext()
  return <div>Current route: {path}</div>
}

4️⃣ useNavigation()

Returns router state: loading, current path, and pending path.

import { useNavigation } from "routexiz"

function NavStatus() {
  const { loading, path, pendingPath } = useNavigation()
  return (
    <div>
      {loading ? `Navigating to ${pendingPath}...` : `Current path: ${path}`}
    </div>
  )
}

5️⃣ useTransition()

Returns transition info (name and stage). Useful for page animations.

import { useTransition } from "routexiz"

function PageTransition() {
  const { name, stage } = useTransition()
  return <div>Transition {name}: {stage}</div>
}
.fade-entering {
  opacity: 0;
  transition: opacity 0.3s ease;
}

.fade-exiting {
  opacity: 1;
}

You are responsible for defining CSS transitions for each stage.

6️⃣ useRouterData()

import React from "react"
import { useRouterData } from "./hooks"

export function UserPage() {
  const { data, params, query, path, meta } = useRouterData<{ id: string; name: string }>()

  return (
    <div>
      <h2>User Page</h2>
      <div>Path: {path}</div>
      <div>Params: {JSON.stringify(params)}</div>
      <div>Query: {JSON.stringify(query)}</div>
      <div>Data: {JSON.stringify(data)}</div>
      <div>Meta: {JSON.stringify(meta)}</div>
    </div>
  )
}

7️⃣ useQuery()

import { useQuery } from "routexiz"

function Users() {
  const query = useQuery()

  return (
    <div>
      Tab: {query.tab}
    </div>
  )
}

RouterProvider

transition

import "routexiz/styles.css"
export default function App() {
  return <RouterProvider transition="fade" duration={100} />;
}

Support fade | slide | scale | none


transition with Wrapper

export default function App() {
  return <RouterProvider transition="fade" duration={100} wrapper={Layout} />;
}

Data Preloading (SSR / Advanced)

routexiz provides loadRouteData() to preload all route loaders before rendering.

This is useful for:

  • SSR (Server-Side Rendering)
  • Prefetching with full data
  • Avoiding loading flicker

1️⃣ Basic Usage

import { loadRouteData } from "routexiz"

const data = await loadRouteData("/users/1")

2️⃣ Example Route

route("/users/:id", User, {
  loader: async ({ params }) => {
    await new Promise(r => setTimeout(r, 300))
    return { id: params.id, name: "User " + params.id }
  }
})

3️⃣ Access Loaded Data

loadRouteData returns a map of resources (Suspense-based):

const data = await loadRouteData("/users/1")

const user = data["/users/:id"].get()

console.log(user)
// { id: "1", name: "User 1" }

4️⃣ SSR Example

import { loadRouteData } from "routexiz"

async function render(url: string) {
  const data = await loadRouteData(url)

  const user = data["/users/:id"]?.get()

  return `
    <html>
      <body>
        <h1>${user?.name}</h1>
      </body>
    </html>
  `
}

render("/users/1")

5️⃣ Nested Routes Example

route("/", Layout, root => {
  root.route("/dashboard", Dashboard, dash => {
    dash.route("/users/:id", User, {
      loader: async ({ params }) => {
        return { id: params.id }
      }
    })
  })
})
const data = await loadRouteData("/dashboard/users/1")

const user = data["/dashboard/users/:id"].get()

6️⃣ Notes

  • Each route loader returns a resource (Suspense-based)

  • Use .get() to read data safely

  • .read() is used internally to trigger loading

7️⃣ Difference vs prefetch

| Function | Behavior | | ----------------- | ------------------------------ | | prefetch() | Trigger loading (non-blocking) | | loadRouteData() | Await full data (blocking) |

When to use

  • ✅ SSR (recommended)
  • ✅ Full preloading before navigation
  • ✅ Testing / debugging loaders

Comparison

routexiz focuses on modern React routing with Suspense-first data loading, nested routes, guards, middleware, and prefetch/caching.
It’s lightweight and flexible, designed for client-side SPAs.

| Criteria | routexiz | React Router | TanStack Router | Remix | | ------------------------- | -------- | ------------ | --------------- | ----- | | Nested routes builder API | ✅ | ✅ | ✅ | ✅ | | Suspense-first loaders | ✅ | ⚠️ | ✅ | ✅ | | Guards & middleware | ✅ | ⚠️ | ✅ | ⚠️ | | Prefetch / caching | ✅ | ❌ | ✅ | ⚠️ | | Error boundary per route | ✅ | ⚠️ | ✅ | ✅ | | Transition support | ✅ | ❌ | ⚠️ | ⚠️ | | Lightweight & minimal | ✅ | ⚠️ | ⚠️ | ⚠️ |


Full Example

Users.tsx

// Users.tsx
import { Link } from "routexiz";

export function Users() {
  return (
    <div>
      <h3>Users List</h3>
      <Link to="/users/1" query={{ tab: [1, 2] }}>User 1</Link>
      <br />
      <Link to="/users/2">User 2</Link>
      <br />
      <Link to="/users/999">User 999 (error)</Link>
      <br />
      <Link to="/users/1/profile">User 1 Profile</Link>
    </div>
  );
}

App.tsx

import React, { ReactNode, Suspense } from "react";
import {
  Link,
  RouterProvider,
  route,
  useLoaderData,
  useParams,
  useQuery,
} from "routexiz";

import "routexiz/styles.css";

/* =========================
   DEMO PAGES
========================= */

function Layout({ children }: { children?: ReactNode }) {
  return (
    <div style={{ padding: 20 }}>
      <h2>App Layout</h2>
      <nav>
        <Link to="/dashboard">Dashboard</Link> |{" "}
        <Link to="/dashboard/stats">Dashboard Stats</Link> |{" "}
        <Link to="/dashboard/settings">Dashboard Settings</Link> |{" "}
        <Link to="/users">Users</Link> |{" "}
        <Link to="/users/1">User 1</Link> |{" "}
        <Link to="/about">About</Link> |{" "}
        <Link to="/about/team">Team</Link> |{" "}
        <Link to="/contact">Contact</Link> |{" "}
        <Link to="/blog/my-first-post">Blog Post</Link> |{" "}
        <Link to="/search/react#top">Search React</Link> |{" "}
        <Link to="/unknown/path">Unknown (404)</Link>
      </nav>
      <hr />
      {children}
    </div>
  );
}

function Dashboard({ children }: { children?: ReactNode }) {
  return <div>Dashboard {children}</div>;
}

// Lazy-loaded Users list
const Users = React.lazy(() => import("./Users"));

function User() {
  const data = useLoaderData<any>();
  const params = useParams();
  const query = useQuery();

  return (
    <div>
      <h3>User {params.id}</h3>
      <div>Query: {JSON.stringify(query)}</div>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
}

function About({ children }: { children?: ReactNode }) {
  return (
    <div>
      <h2>About Page</h2>
      {children}
    </div>
  );
}

function Team() {
  return <div>Team Page</div>;
}

function Contact() {
  return <div>Contact Page</div>;
}

function Blog() {
  const data = useLoaderData<any>();
  return <div>Blog Post: {data.slug}</div>;
}

function Search() {
  const params = useParams();
  const query = useQuery();
  const term = Array.isArray(query.term) ? query.term[0] : query.term;

  return (
    <div>
      <h3>Search Page</h3>
      <div>Param: {params.term || "none"}</div>
      <div>Query: {term || "none"}</div>
    </div>
  );
}

function NotFound() {
  return <div>Page Not Found (404)</div>;
}

/* =========================
   ROUTES SETUP
========================= */
function setupRoutes() {
  route("/", Layout, (root) => {
    // Dashboard + nested
    root.route("/dashboard", Dashboard, (dash) => {
      dash.guard(({ path }) => {
        console.log("Guard dashboard:", path);
        return true;
      });
      dash.middleware(({ path }) => console.log("Middleware dashboard:", path));
      dash.onEnter(({ path }) => console.log("Enter dashboard:", path));
      dash.onLeave(({ path }) => console.log("Leave dashboard:", path));

      dash.route("/stats", () => <div>Dashboard Stats Page</div>);
      dash.route("/settings", () => <div>Dashboard Settings Page</div>);
    });

    // Users list
    root.route("/users", Users); // lazy page

    // Dynamic user pages
    root.route("/users/:id", User, {
      loader: async ({ params }) => {
        await new Promise((r) => setTimeout(r, 400));
        if (params.id === "999") throw new Error("User not found");
        return { id: params.id, name: "User " + params.id };
      },
      fallback: <div>Loading user...</div>,
      errorBoundary: ({ error }) => <div>Error: {String(error)}</div>,
    });

    // Profile & posts subroutes
    root.route("/users/:id/profile", () => <div>User Profile Page</div>);
    root.route("/users/:id/posts", () => <div>User Posts Page</div>);

    // About nested
    root.route("/about", About, (about) => {
      about.route("team", Team);
    });

    root.route("/contact", Contact);

    // wildcard fallback
    root.route("*", NotFound);
  });

  // Blog route with loader
  route("/blog/:slug", Blog, {
    loader: async ({ params }) => {
      await new Promise((r) => setTimeout(r, 200));
      return { slug: params.slug };
    },
  });

  // Search route
  route("/search", Search);
  route("/search/:term", Search);
}

setupRoutes();

/* =========================
   APP
========================= */
export default function App() {
  return (
    <div>
      <h1>Full RouteXiz Demo</h1>
      <Suspense fallback={<div>Loading...</div>}>
        <RouterProvider />
      </Suspense>
    </div>
  );
}

Architecture

Link / navigate / redirect
   ↓
matchRouteChain
   ↓
guards → middleware
   ↓
loader (cached)
   ↓
Suspense
   ↓
render

Duplicate Routes

routexiz does not support duplicate route paths.

Each route must resolve to a unique full path in the route tree.

Why?

  • Navigation resolves a single path from root to leaf
  • Duplicate paths create ambiguous matches
  • This leads to unpredictable behavior
  • Duplicate routes are NOT validated automatically.
  • The router will NOT throw an error or warning.

Requirement

Ensure all routes are unique:

route("/", Layout, root => {
  root.route("/dashboard", Dashboard)
  root.route("/dashboard/users", Users)

  // ❌ BAD: duplicate
  root.route("/dashboard/users", AnotherUsers)
})

Recommendation

  • Keep route paths explicit and unique
  • Avoid defining the same full path in multiple branches
  • Use nesting correctly to prevent duplication

License

MIT