Skip to content

Repository files navigation

🐯 Tiger Router

A tiny, typed router for React applications that do not need a framework.

npm version CI bundle size React 18 and 19 TypeScript License: ISC

Zero runtime dependencies Β· Concurrent-safe Β· ESM + CJS Β· RSC-ready

Quick start Β· Why Tiger Router? Β· Recipes Β· API Β· Migrate from v2 Β· Changelog

Quick start

npm install tiger-router
import { Router, Routes, Route, Link } from 'tiger-router'

export default function App() {
    return (
        <Router>
            <nav>
                <Link to="/">Home</Link>
                <Link to="/about">About</Link>
            </nav>

            <Routes fallback={<h1>404</h1>}>
                <Route path="/" element={<h1>Home</h1>} />
                <Route path="/about" element={<h1>About</h1>} />
            </Routes>
        </Router>
    )
}

That is the whole mental model: Router holds the state, Routes picks one match, Route describes a page, and Link navigates without a reload.


Why Tiger Router?

  • ~2.6 kB brotli for a typical app, zero runtime dependencies
  • React 18 and 19, built on useSyncExternalStore β€” safe under concurrent rendering
  • TypeScript first, with typed params: useParams<{ id: string }>()
  • Works in RSC setups (Next.js App Router) β€” ships the 'use client' directive
  • 6 components, 6 hooks. A deliberately small API you can learn in minutes
  • No config, no route objects, no build step

Is it the right router for your app?

Choose Tiger Router when… Choose a full routing framework when…
You want client-side routing with a very small API You need route-level loaders, actions or middleware
Bundle size and zero dependencies matter You want file-based route generation
You prefer JSX routes and standard browser APIs You need built-in caching or search-param schemas
You are building an SPA, widget, prototype or small product Routing is the architectural core of a large application

Tiger Router intentionally does not include data loaders, route guards or custom code-splitting APIs. Compose those with React, or use React Router or TanStack Router when you need a full routing framework.


Learn it in five minutes

1. URL params

Write :name in the path, read it with useParams:

import { Route, Routes, useParams } from 'tiger-router'

function User() {
    const { id } = useParams<{ id: string }>()
    return <h1>User {id}</h1>
}

<Routes>
    <Route path="/users/:id" element={<User />} />
</Routes>
Pattern Matches Params
/about /about, /about/ {}
/users/:id /users/42 { id: '42' }
/posts/:page? /posts, /posts/2 { page: undefined } or '2'
/files/* /files/a/b.pdf { '*': 'a/b.pdf' }
* anything { '*': '...' }

Params are URL-decoded for you, and static segments match case-insensitively.

2. Order does not matter

<Routes> renders the most specific match, so you can declare routes in any order:

<Routes>
    <Route path="*" element={<NotFound />} />          {/* least specific */}
    <Route path="/users/:id" element={<User />} />
    <Route path="/users/new" element={<NewUser />} />  {/* wins for /users/new */}
</Routes>

3. Navigating in code

import { useNavigate } from 'tiger-router'

function LoginForm() {
    const navigate = useNavigate()

    async function onSubmit() {
        await login()
        navigate('/dashboard', { replace: true }) // no back-button trap
    }
}

navigate(-1) goes back, navigate(1) goes forward.

4. The query string as state

import { useSearchParams } from 'tiger-router'

function Search() {
    const [params, setParams] = useSearchParams()
    const q = params.get('q') ?? ''

    return (
        <input
            value={q}
            onChange={e => setParams({ q: e.target.value }, { replace: true })}
        />
    )
}

Reload the page and the search box is still filled in β€” the URL was the state all along.

5. Active navigation links

import { NavLink } from 'tiger-router'

<NavLink to="/about" className={({ isActive }) => (isActive ? 'current' : '')}>
    About
</NavLink>

NavLink also sets aria-current="page", so screen readers announce the current page. Add end to only light up on an exact match.

6. Nested routes

End a path with * and the child component gets the rest of the URL:

function App() {
    return (
        <Routes>
            <Route path="/settings/*" element={<Settings />} />
        </Routes>
    )
}

function Settings() {
    return (
        <>
            <h1>Settings</h1>
            {/* These paths are relative to /settings */}
            <Routes fallback={<Overview />}>
                <Route path="/profile" element={<Profile />} />
                <Route path="/billing/:plan" element={<Billing />} />
            </Routes>
        </>
    )
}

Inside a nested scope, relative links just work: <Link to="profile"> from /settings goes to /settings/profile. Params from parent routes are inherited by children.


Recipes

404 page
<Routes fallback={<NotFound />}>{/* ... */}</Routes>

Or, if you prefer it as a route: <Route path="*" element={<NotFound />} />.

Redirect
import { Navigate } from 'tiger-router'

<Route path="/old-pricing" element={<Navigate to="/pricing" />} />
Protected route
function RequireAuth({ children }: { children: React.ReactNode }) {
    const { user } = useAuth()
    const location = useLocation()

    if (!user) return <Navigate to="/login" state={{ from: location.pathname }} />
    return <>{children}</>
}

<Route path="/dashboard" element={<RequireAuth><Dashboard /></RequireAuth>} />

Read the origin back on the login page with useLocation().state.

Code splitting with lazy + Suspense
const Dashboard = lazy(() => import('./Dashboard'))

<Suspense fallback={<Spinner />}>
    <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
    </Routes>
</Suspense>
Static hosting (GitHub Pages, S3) β€” hash mode
<Router mode="hash">…</Router>

URLs become /#/about, which needs no server rewrites.

App served from a subfolder
<Router base="/docs">…</Router>

Now <Link to="/api"> points the browser at /docs/api, while useLocation().pathname stays /api.

Testing

memory mode keeps the URL out of the browser, so tests never leak state:

render(
    <Router mode="memory" initialPath="/users/42">
        <App />
    </Router>
)
Next.js App Router / React Server Components

The published files carry the 'use client' directive, so importing Tiger Router from a client component works without extra setup. On the server the router falls back to memory mode instead of touching window.


API

Components

Component Props
Router mode?: 'browser' | 'hash' | 'memory', base?, initialPath?, history?, children
Routes children (<Route> elements), fallback?
Route path, element?, children?
Link to, replace?, state? + every <a> attribute
NavLink Link props, plus end? and function forms of className / style / children
Navigate to, replace? (defaults to true), state?

<Route> also works on its own, outside <Routes> β€” then every matching route renders, which is handy for persistent UI like a sidebar.

Hooks

Hook Returns
useLocation() { pathname, search, hash, state, key }
useNavigate() (to, { replace, state }) => void, or (delta: number)
useParams<T>() Params of this route and its ancestors
useSearchParams() [URLSearchParams, setSearchParams]
useMatch(pattern) The match (with params) or null
useRouter() { location, history, basename, routeBase } β€” escape hatch

Core (no React)

createHistory, matchRoute, rankMatches, parsePath, joinPaths, resolvePath, toHref are exported too, in case you want route matching outside React.


Migrating from v2

- <Router>
-     <Route path="/" element={<Home />} />
-     <Route path="/users/:id" element={<User />} />
- </Router>
+ <Router>
+     <Routes fallback={<NotFound />}>
+         <Route path="/" element={<Home />} />
+         <Route path="/users/:id" element={<User />} />
+     </Routes>
+ </Router>
  • Wrap your routes in <Routes> to get single-match rendering and a 404
  • mode="history" is now mode="browser"
  • useRouteMatch(path) is now useMatch(path) and returns the match object (or null) instead of a boolean
  • Everything else keeps working. See CHANGELOG.md for the full list.

Local development

npm install
npm run dev       # demo app on http://localhost:5173
npm test          # unit + render tests
npm run verify    # lint, types, tests, build, package checks, size budget

Contributions are welcome β€” read the contribution guide, report a bug or start a discussion in an issue.

Cheers 🍻

About

Tiger Router offers a minimalist-friendly routing solution for your React applications 🐯

Topics

Resources

Contributing

Stars

5 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages