# 🕹️ routexiz

[![NPM](https://img.shields.io/npm/v/routexiz.svg)](https://www.npmjs.com/package/routexiz) ![Downloads](https://img.shields.io/npm/dt/routexiz.svg) ![Bundle Size](https://img.shields.io/bundlephobia/minzip/routexiz)

<a href="https://codesandbox.io/p/sandbox/2lslks" target="_blank">LIVE EXAMPLE</a>

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:

```ts
navigate("/users/1")
```

`routexiz`:

```ts
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

```bash
npm install routexiz
```

---

# Basic

```tsx
import { route, RouterProvider } from "routexiz"

route("/", () => <div>Home</div>)

export default function App() {
  return <RouterProvider />
}
```

---

# Nested Routing & Builder API

```ts
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

```ts
const navigate = useNavigate()

navigate("/users/:id", {
  params: { id: 1 },
  query: { tab: "profile" }

  // transition: 'fade',
  // duration: 100
})
```

---

## redirect

```ts
import { redirect } from "routexiz";

redirect("/users/:id", { params: { id: 1 } });
```

---

## Link

```tsx
<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:

```ts
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

```ts
// 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

```ts
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

```ts
dash.middleware(async ({ params, query }) => {
  console.log("Visited dashboard with params:", params);
});
```

---

## 3️⃣ onEnter / onLeave

Hooks triggered when entering or leaving a route node:
```ts
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


<b> Guards - Middleware</b>

| 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                |

<br />

<b>Mental Model</b>
- Guard = "Can we enter?"
- Middleware = "Do something while entering"

---

## 4️⃣ prefetch

Prefetch loader data in advance:

```tsx
<Link to="/users/:id" params={{ id: 1 }}>User 1</Link>
```

- Hover → prefetch by default
- Viewport → prefetch when enters viewport
- Disable: disablePrefetch

---

## 5️⃣ errorBoundary / fallback

```ts
root.route("/users/:id", User, {
  fallback: <div>Loading user...</div>,
  errorBoundary: ({ error }) => <div>Error: {String(error)}</div>,
});
```

---

# Lazy

```ts
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
```ts
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
```ts
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
```ts
import { prefetch } from "routexiz"

// Prefetch user 1 in background
prefetch("/users/1")
```

## 4️⃣ Cleanup expired cache
```ts
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
```ts
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
```ts
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
```ts
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.

```ts
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.
```ts
import { useParams } from "routexiz"

function User() {
  const params = useParams()
  return <div>User ID: {params.id}</div>
}
```

## 3️⃣ useRouteContext()

Returns the current route path.

```ts
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.

```ts
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.

```ts
import { useTransition } from "routexiz"

function PageTransition() {
  const { name, stage } = useTransition()
  return <div>Transition {name}: {stage}</div>
}
```

```css
.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()

```ts
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()
```ts
import { useQuery } from "routexiz"

function Users() {
  const query = useQuery()

  return (
    <div>
      Tab: {query.tab}
    </div>
  )
}
```

---

# RouterProvider
## transition

```ts
import "routexiz/styles.css"
export default function App() {
  return <RouterProvider transition="fade" duration={100} />;
}
```

Support `fade` | `slide` | `scale` | `none`

---

## transition with Wrapper

```ts
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
```ts
import { loadRouteData } from "routexiz"

const data = await loadRouteData("/users/1")
```

## 2️⃣ Example Route

```ts
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):

```ts
const data = await loadRouteData("/users/1")

const user = data["/users/:id"].get()

console.log(user)
// { id: "1", name: "User 1" }
```

## 4️⃣ SSR Example
```ts
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
```ts
route("/", Layout, root => {
  root.route("/dashboard", Dashboard, dash => {
    dash.route("/users/:id", User, {
      loader: async ({ params }) => {
        return { id: params.id }
      }
    })
  })
})

```

```ts
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

```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

```ts
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

```text
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:

```ts
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
