Skip to content

molefrog/wouter

Repository files navigation

Wouter — a super-tiny React router (logo by Katya Simacheva)

npm CI Coverage Coverage Edit in StackBlitz IDE
wouteris a tiny router for modern React and Preact apps that relies on Hooks.
A router you wanted so bad in your project!

Features

⚠️These docs are for wouter v3 only. Please find the documentation for[email protected] here

by Katya Simacheva

developers 💖 wouter

... I love Wouter. It’s tiny, fully embraces hooks, and has an intuitive and barebones API. I can accomplish everything I could with react-router with Wouter, and it just feelsmore minimalist while not being inconvenient.

Matt Miller,An exhaustive React ecosystem for 2020

Wouter provides a simple API that many developers and library authors appreciate. Some notable projects that use wouter:Ultra, React-three-fiber, Sunmao UI,Millionand many more.

Table of Contents

Getting Started

First, add wouter to your project.

npm i wouter

Or, if you're using Preact the use the following commandnpm i wouter-preact.

Check out this simple demo app below. It doesn't cover hooks and other features such as nested routing, but it's a good starting point for those who are migrating from React Router.

import{Link,Route,Switch}from"wouter";

constApp=()=>(
<>
<Linkhref="/users/1">Profile</Link>

<Routepath="/about">About Us</Route>

{/*
Routes below are matched exclusively -
the first matched route gets rendered
*/}
<Switch>
<Routepath="/inbox"component={InboxPage}/>

<Routepath="/users/:name">
{(params)=><>Hello,{params.name}!</>}
</Route>

{/* Default route in a switch */}
<Route>404: No such page!</Route>
</Switch>
</>
);

Browser Support

This library is designed forES2020+compatibility. If you need to support older browsers, make sure that you transpilenode_modules.Additionally, the minimum supported TypeScript version is 4.1 in order to support route parameter inference.

Wouter API

Wouter comes with three kinds of APIs: low-levelstandalone location hooks,hooks forrouting and pattern matchingand more traditionalcomponent-based APIsimilar to React Router's one.

You are free to choose whatever works for you: use location hooks when you want to keep your app as small as possible and don't need pattern matching; use routing hooks when you want to build custom routing components; or if you're building a traditional app with pages and navigation — components might come in handy.

Check out alsoFAQ and Code Recipesfor more advanced things like active links, default routes, server-side rendering etc.

The list of methods available

Location Hooks

These can be used separately from the main module and have an interface similar touseState.These hooks don't support nesting, base path, route matching.

Routing Hooks

Import fromwoutermodule.

  • useRoute— shows whether or not current page matches the pattern provided.
  • useLocation— allows to manipulate current router's location, by default subscribes to browser location.Note:this isn't the same asuseBrowserLocation,read below.
  • useParams— returns an object with parameters matched from the closest route.
  • useSearch— returns a search string – everything that goes after the?.
  • useRouter— returns a global router object that holds the configuration. Only use it if you want to customize the routing.

Components

Import fromwoutermodule.

  • <Route />— conditionally renders a component based on a pattern.
  • <Link />— wraps<a>,allows to perfom a navigation.
  • <Switch />— exclusive routing, only renders the first matched route.
  • <Redirect />— when rendered, performs an immediate navigation.
  • <Router />— an optional top-level component for advanced routing configuration.

Hooks API

useRoute:route matching and parameters

Checks if the current location matches the pattern provided and returns an object with parameters. This is powered by a wonderfulregexparamlibrary, so all its pattern syntax is fully supported.

You can useuseRouteto perform manual routing or implement custom logic, such as route transitions, etc.

import{useRoute}from"wouter";

constUsers=()=>{
// `match` is a boolean
const[match,params]=useRoute("/users/:name");

if(match){
return<>Hello,{params.name}!</>;
}else{
returnnull;
}
};

A quick cheatsheet of what types of segments are supported:

useRoute("/app/:page");
useRoute("/app/:page/:section");

// optional parameter, matches "/en/home" and "/home"
useRoute("/:locale?/home");

// suffixes
useRoute("/movies/:title.(mp4|mov)");

// wildcards, matches "/app", "/app-1", "/app/home"
useRoute("/app*");

// optional wildcards, matches "/orders", "/orders/"
// and "/orders/completed/list"
useRoute("/orders/*?");

// regex for matching complex patterns,
// matches "/hello:123"
useRoute(/^[/]([a-z]+):([0-9]+)[/]?$/);
// and with named capture groups
useRoute(/^[/](?<word>[a-z]+):(?<num>[0-9]+)[/]?$/);

The second item in the pairparamsis an object with parameters or null if there was no match. For wildcard segments the parameter name is"*":

// wildcards, matches "/app", "/app-1", "/app/home"
const[match,params]=useRoute("/app*");

if(match){
// "/home" for "/app/home"
constpage=params["*"];
}

useLocation:working with the history

To get the current path and navigate between pages, call theuseLocationhook. Similarly touseState,it returns a value and a setter: the component will re-render when the location changes and by callingnavigateyou can update this value and perform navigation.

By default, it usesuseBrowserLocationunder the hood, though you can configure this in a top-levelRoutercomponent (for example, if you decide at some point to switch to a hash-based routing).useLocationwill also return scoped path when used within nested routes or with base path setting.

import{useLocation}from"wouter";

constCurrentLocation=()=>{
const[location,setLocation]=useLocation();

return(
<div>
{`The current page is:${location}`}
<aonClick={()=>setLocation("/somewhere")}>Click to update</a>
</div>
);
};

All the components internally call theuseLocationhook.

Additional navigation parameters

The setter method ofuseLocationcan also accept an optional object with parameters to control how the navigation update will happen.

When browser location is used (default),useLocationhook acceptsreplaceflag to tell the hook to modify the current history entry instead of adding a new one. It is the same as callingreplaceState.

const[location,navigate]=useLocation();

navigate("/jobs");// `pushState` is used
navigate("/home",{replace:true});// `replaceState` is used

Additionally, you can provide astateoption to updatehistory.statewhile navigating:

navigate("/home",{state:{modal:"promo"}});

history.state;// { modal: "promo" }

Customizing the location hook

By default,wouterusesuseLocationhook that reacts topushStateandreplaceState navigation viauseBrowserLocation.

To customize this, wrap your app in aRoutercomponent:

import{Router,Route}from"wouter";
import{useHashLocation}from"wouter/use-hash-location";

constApp=()=>(
<Routerhook={useHashLocation}>
<Routepath="/about"component={About}/>
...
</Router>
);

Because these hooks have return values similar touseState,it is easy and fun to build your own location hooks:useCrossTabLocation,useLocalStorage,useMicroFrontendLocationand whatever routing logic you want to support in the app. Give it a try!

useParams:extracting matched parameters

This hook allows you to access the parameters exposed throughmatching dynamic segments.Internally, we simply wrap your components in a context provider allowing you to access this data anywhere within theRoutecomponent.

This allows you to avoid "prop drilling" when dealing with deeply nested components within the route.Note:useParamswill only extract parameters from the closest parent route.

import{Route,useParams}from"wouter";

constUser=()=>{
constparams=useParams();

params.id;// "1"

// alternatively, use the index to access the prop
params[0];// "1"
};

<Routepath="/user/:id"component={User}>/>

It is the same for regex paths. Capture groups can be accessed by their index, or if there is a named capture group, that can be used instead.

import{Route,useParams}from"wouter";

constUser=()=>{
constparams=useParams();

params.id;// "1"
params[0];// "1"
};

<Routepath={/^[/]user[/](?<id>[0-9]+)[/]?$/}component={User}>/>

useSearch:query strings

Use this hook to get the current search (query) string value. It will cause your component to re-render only when the string itself and not the full location updates. The search string returneddoes notcontain a?character.

import{useSearch}from"wouter";

// returns "tab=settings&id=1"
// the hook for extracting search parameters is coming soon!
constsearchString=useSearch();

For the SSR, usessrSearchprop passed to the router.

<RouterssrSearch={request.search}>{/* SSR! */}</Router>

Refer toServer-Side Renderingfor more info on rendering and hydration.

useRouter:accessing the router object

If you're building advanced integration, for example custom location hook, you might want to get access to the global router object. Router is a simple object that holds routing options that you configure in theRoutercomponent.

import{useRouter}from"wouter";

constCustom=()=>{
constrouter=useRouter();

router.hook;// `useBrowserLocation` by default
router.base;// "/app"
};

constApp=()=>(
<Routerbase="/app">
<Custom/>
</Router>
);

Component API

<Route path={pattern} />

Routerepresents a piece of the app that is rendered conditionally based on a patternpath.Pattern has the same syntax as the argument you pass touseRoute.

The library provides multiple ways to declare a route's body:

import{Route}from"wouter";

// simple form
<Routepath="/home"><Home/></Route>

// render-prop style
<Routepath="/users/:id">
{params=><UserPageid={params.id}/>}
</Route>

// the `params` prop will be passed down to <Orders />
<Routepath="/orders/:status"component={Orders}/>

A route with no path is considered to always match, and it is the same as<Route path= "*" />.When developing your app, use this trick to peek at the route's content without navigation.

-<Route path= "/some/page" >
+<Route>
{/* Strip out the `path` to make this visible */}
</Route>

Route Nesting

Nesting is a core feature of wouter and can be enabled on a route via thenestprop. When this prop is present, the route matches everything that starts with a given pattern and it creates a nested routing context. All child routes will receive location relative to that pattern.

Let's take a look at this example:

<Routepath="/app"nest>
<Routepath="/users/:id"nest>
<Routepath="/orders"/>
</Route>
</Route>
  1. This first route will be active for all paths that start with/app,this is equivalent to having a base path in your app.

  2. The second one uses dynamic pattern to match paths like/app/user/1,/app/user/1/anythingand so on.

  3. Finally, the inner-most route will only work for paths that look like/app/users/1/orders.The match is strict, since that route does not have anestprop and it works as usual.

If you calluseLocation()inside the last route, it will return/ordersand not/app/users/1/orders.This creates a nice isolation and it makes it easier to make changes to parent route without worrying that the rest of the app will stop working. If you need to navigate to a top-level page however, you can use a prefix~to refer to an absolute path:

<Routepath="/payments"nest>
<Routepath="/all">
<Linkto="~/home">Back to Home</Link>
</Route>
</Route>

Note:Thenestprop does not alter the regex passed into regex paths. Instead, thenestprop will only determine if nested routes will match against the rest of path or the same path. To make a strict path regex, use a regex pattern like/^[/](your pattern)[/]?$/(this matches an optional end slash and the end of the string). To make a nestable regex, use a regex pattern like/^[/](your pattern)(?=$|[/])/(this matches either the end of the string or a slash for future segments).

<Link href={path} />

Link component renders an<a />element that, when clicked, performs a navigation.

import{Link}from"wouter"

<Linkhref="/">Home</Link>

// `to` is an alias for `href`
<Linkto="/">Home</Link>

// all standard `a` props are proxied
<Linkhref="/"className="link"aria-label="Go to homepage">Home</Link>

// all location hook options are supported
<Linkhref="/"replacestate={{animate:true}}/>

Link will always wrap its children in an<a />tag, unlessasChildprop is provided. Use this when you need to have a custom component that renders an<a />under the hood.

// use this instead
<Linkto="/"asChild>
<UIKitLink/>
</Link>

// Remember, `UIKitLink` must implement an `onClick` handler
// in order for navigation to work!

When you pass a function as aclassNameprop, it will be called with a boolean value indicating whether the link is active for the current route. You can use this to style active links (e.g. for links in navigation menu)

<LinkclassName={(active)=>(active?"active":"")}>Nav</Link>

Read more aboutactive links here.

<Switch />

There are cases when you want to have an exclusive routing: to make sure that only one route is rendered at the time, even if the routes have patterns that overlap. That's whatSwitchdoes: it only rendersthe first matching route.

import{Route,Switch}from"wouter";

<Switch>
<Routepath="/orders/all"component={AllOrders}/>
<Routepath="/orders/:status"component={Orders}/>

{/*
in wouter, any Route with empty path is considered always active.
This can be used to achieve "default" route behaviour within Switch.
Note: the order matters! See examples below.
*/}
<Route>This is rendered when nothing above has matched</Route>
</Switch>;

When no route in switch matches, the last emptyRoutewill be used as a fallback. SeeFAQ and Code Recipessectionto read about default routes.

<Redirect to={path} />

When mounted performs a redirect to apathprovided. UsesuseLocationhook internally to trigger the navigation inside of auseEffectblock.

Redirectcan also accept props forcustomizing how navigation will be performed,for example for setting history state when navigating. These options are specific to the currently used location hook.

<Redirectto="/"/>

// arbitrary state object
<Redirectto="/"state={{modal:true}}/>

// use `replaceState`
<Redirectto="/"replace/>

If you need more advanced logic for navigation, for example, to trigger the redirect inside of an event handler, consider using useLocationhook instead:

import{useLocation}from"wouter";

const[location,setLocation]=useLocation();

fetchOrders().then((orders)=>{
setOrders(orders);
setLocation("/app/orders");
});

<Router hook={hook} parser={fn} base={basepath} hrefs={fn} />

UnlikeReact Router,routes in wouterdon't have to be wrapped in a top-level component.An internal router object will be constructed on demand, so you can start writing your app without polluting it with a cascade of top-level providers. There are cases however, when the routing behaviour needs to be customized.

These cases include hash-based routing, basepath support, custom matcher function etc.

import{useHashLocation}from"wouter/use-hash-location";

<Routerhook={useHashLocation}base="/app">
{/* Your app goes here */}
</Router>;

A router is a simple object that holds the routing configuration options. You can always obtain this object using auseRouterhook.The list of currently available options:

  • hook: () => [location: string, setLocation: fn]— is a React Hook function that subscribes to location changes. It returns a pair of currentlocationstring e.g./app/usersand a setLocationfunction for navigation. You can use this hook from any component of your app by callinguseLocation()hook.SeeCustomizing the location hook.

  • searchHook: () => [search: string, setSearch: fn]— similar tohook,but for obtaining thecurrent search string.

  • base: string— an optional setting that allows to specify a base path, such as/app.All application routes will be relative to that path. To navigate out to an absolute path, prefix your path with an~.See the FAQ.

  • parser: (path: string, loose?: boolean) => { pattern, keys }— a pattern parsing function. Produces a RegExp for matching the current location against the user-defined patterns like /app/users/:id.Has the same interface as theparsefunction fromregexparam.Seethis examplethat demonstrates custom parser feature.

  • ssrPath: stringandssrSearch: stringuse these whenrendering your app on the server.

  • hrefs: (href: boolean) => string— a function for transforminghrefattribute of an<a />element rendered byLink.It is used to support hash-based routing. By default,hrefattribute is the same as thehrefortoprop of aLink.A location hook can also define ahook.hrefsproperty, in this case thehrefwill be inferred.

FAQ and Code Recipes

I deploy my app to the subfolder. Can I specify a base path?

You can! Wrap your app with<Router base= "/app" />component and that should do the trick:

import{Router,Route,Link}from"wouter";

constApp=()=>(
<Routerbase="/app">
{/* the link's href attribute will be "/app/users" */}
<Linkhref="/users">Users</Link>

<Routepath="/users">The current path is /app/users!</Route>
</Router>
);

CallinguseLocation()within a route in an app with base path will return a path scoped to the base. Meaning that when base is"/app"and pathname is"/app/users"the returned string is"/users".Accordingly, callingnavigatewill automatically append the base to the path argument for you.

When you have multiple nested routers, base paths are inherited and stack up.

<Routerbase="/app">
<Routerbase="/cms">
<Routepath="/users">Path is /app/cms/users!</Route>
</Router>
</Router>

How do I make a default route?

One of the common patterns in application routing is having a default route that will be shown as a fallback, in case no other route matches (for example, if you need to render 404 message). In wouterthis can easily be done as a combination of<Switch />component and a default route:

import{Switch,Route}from"wouter";

<Switch>
<Routepath="/about">...</Route>
<Route>404, Not Found!</Route>
</Switch>;

Note:the order of switch children matters, default route should always come last.

If you want to have access to the matched segment of the path you can use wildcard parameters:

<Switch>
<Routepath="/users">...</Route>

{/* will match anything that starts with /users/, e.g. /users/foo, /users/1/edit etc. */}
<Routepath="/users/*">...</Route>

{/* will match everything else */}
<Routepath="*">
{(params)=>`404, Sorry the page${params["*"]}does not exist!`}
</Route>
</Switch>

▶ Demo Sandbox

How do I make a link active for the current route?

Instead of a regularclassNamestring, provide a function to use custom class when this link matches the current route. Note that it will always perform an exact match (i.e./userswill not be active for/users/1).

<LinkclassName={(active)=>(active?"active":"")}>Nav link</Link>

If you need to control other props, such asaria-currentorstyle,you can write your own<Link />wrapper and detect if the path is active by using theuseRoutehook.

const[isActive]=useRoute(props.href);

return(
<Link{...props}asChild>
<astyle={isActive?{color:"red"}:{}}>{props.children}</a>
</Link>
);

▶ Demo Sandbox

Are strict routes supported?

If a trailing slash is important for your app's routing, you could specify a custom parser. Parser is a method that takes a pattern string and returns a RegExp and an array of parsed key. It uses the signature of aparsefunction fromregexparam.

Let's write a custom parser based on a popularpath-to-regexppackage that does support strict routes option.

import{pathToRegexp}from"path-to-regexp";

/**
* Custom parser based on `pathToRegexp` with strict route option
*/
conststrictParser=(path,loose)=>{
constkeys=[];
constpattern=pathToRegexp(path,keys,{strict:true,end:!loose});

return{
pattern,
// `pathToRegexp` returns some metadata about the keys,
// we want to strip it to just an array of keys
keys:keys.map((k)=>k.name),
};
};

constApp=()=>(
<Routerparser={strictParser}>
<Routepath="/foo">...</Route>
<Routepath="/foo/">...</Route>
</Router>
);

▶ Demo Sandbox

Are relative routes and links supported?

Yes! Any route withnestprop present creates a nesting context. Keep in mind, that the location inside a nested route will be scoped.

constApp=()=>(
<Routerbase="/app">
<Routepath="/dashboard"nest>
{/* the href is "/app/dashboard/users" */}
<Linkto="/users"/>

<Routepath="/users">
{/* Here `useLocation()` returns "/users"! */}
</Route>
</Route>
</Router>
);

▶ Demo Sandbox

Can I initiate navigation from outside a component?

Yes, thenavigatefunction is exposed from the"wouter/use-browser-location"module:

import{navigate}from"wouter/use-browser-location";

navigate("/",{replace:true});

It's the same function that is used internally.

Can I usewouterin my TypeScript project?

Yes! Although the project isn't written in TypeScript, the type definition files are bundled with the package.

How can add animated route transitions?

Let's take look at how wouter routes can be animated withframer-motion. Animating enter transitions is easy, but exit transitions require a bit more work. We'll use theAnimatePresencecomponent that will keep the page in the DOM until the exit animation is complete.

Unfortunately,AnimatePresenceonly animates itsdirect children,so this won't work:

import{motion,AnimatePresence}from"framer-motion";

exportconstMyComponent=()=>(
<AnimatePresence>
{/* This will not work! `motion.div` is not a direct child */}
<Routepath="/">
<motion.div
initial={{opacity:0}}
animate={{opacity:1}}
exit={{opacity:0}}
/>
</Route>
</AnimatePresence>
);

The workaround is to match this route manually withuseRoute:

exportconstMyComponent=({isVisible})=>{
const[isMatch]=useRoute("/");

return(
<AnimatePresence>
{isMatch&&(
<motion.div
initial={{opacity:0}}
animate={{opacity:1}}
exit={{opacity:0}}
/>
)}
</AnimatePresence>
);
};

More complex examples involve usinguseRouteshook (similar to how React Router does it), but wouter does not ship it out-of-the-box. Please refer tothis issuefor the workaround.

Preact support?

Preact exports are available through a separate package namedwouter-preact(or within the wouter/preactnamespace, however this method isn't recommended as it requires React as a peer dependency):

-import { useRoute, Route, Switch } from "wouter";
+import { useRoute, Route, Switch } from "wouter-preact";

You might need to ensure you have the latest version of Preact Xwith support for hooks.

▶ Demo Sandbox

Server-side Rendering support (SSR)?

In order to render your app on the server, you'll need to wrap your app with top-level Router and specifyssrPathprop (usually, derived from current request). Optionally,RouteracceptsssrSearchparameter if need to have access to a search string on a server.

import{renderToString}from"react-dom/server";
import{Router}from"wouter";

consthandleRequest=(req,res)=>{
// top-level Router is mandatory in SSR mode
constprerendered=renderToString(
<RouterssrPath={req.path}ssrSearch={req.search}>
<App/>
</Router>
);

// respond with prerendered html
};

Tip: wouter can pre-fillssrSearch,ifssrPathcontains the?character. So these are equivalent:

<RouterssrPath="/goods?sort=asc"/>;

// is the same as
<RouterssrPath="/goods"ssrSearch="sort=asc"/>;

On the client, the static markup must be hydrated in order for your app to become interactive. Note that to avoid having hydration warnings, the JSX rendered on the client must match the one used by the server, so theRoutercomponent must be present.

import{hydrateRoot}from"react-dom/client";

constroot=hydrateRoot(
domNode,
// during hydration, `ssrPath` is set to `location.pathname`,
// `ssrSearch` set to `location.search` accordingly
// so there is no need to explicitly specify them
<Router>
<App/>
</Router>
);

▶ Demo

How do I configure the router to render a specific route in tests?

Testing with wouter is no different from testing regular React apps. You often need a way to provide a fixture for the current location to render a specific route. This can be easily done by swapping the normal location hook withmemoryLocation.It is an initializer function that returns a hook that you can then specify in a top-levelRouter.

import{render}from"@testing-library/react";
import{memoryLocation}from"wouter/memory-location";

it("renders a user page",()=>{
// `static` option makes it immutable
// even if you call `navigate` somewhere in the app location won't change
const{hook}=memoryLocation({path:"/user/2",static:true});

const{container}=render(
<Routerhook={hook}>
<Routepath="/user/:id">{(params)=><>User ID:{params.id}</>}</Route>
</Router>
);

expect(container.innerHTML).toBe("User ID: 2");
});

The hook can be configured to record navigation history. Additionally, it comes with anavigatefunction for external navigation.

it("performs a redirect",()=>{
const{hook,history,navigate}=memoryLocation({
path:"/",
// will store navigation history in `history`
record:true,
});

const{container}=render(
<Routerhook={hook}>
<Switch>
<Routepath="/">Index</Route>
<Routepath="/orders">Orders</Route>

<Route>
<Redirectto="/orders"/>
</Route>
</Switch>
</Router>
);

expect(history).toStrictEqual(["/"]);

navigate("/unknown/route");

expect(container.innerHTML).toBe("Orders");
expect(history).toStrictEqual(["/","/unknown/route","/orders"]);
});

1KB is too much, I can't afford it!

We've got some great news for you! If you're a minimalist bundle-size nomad and you need a damn simple routing in your app, you can just use bare location hooks. For example,useBrowserLocationhook which is only650 bytes gzipped and manually match the current location with it:

import{useBrowserLocation}from"wouter/use-browser-location";

constUsersRoute=()=>{
const[location]=useBrowserLocation();

if(location!=="/users")returnnull;

// render the route
};

Wouter's motto is"Minimalist-friendly".

Acknowledgements

Wouter illustrations and logos were made byKatya Simachevaand Katya Vakulenko.Thank you to@jeetiss and all the amazingcontributorsfor helping with the development.