🥢 A minimalist-friendly ~2.1KB routing for React and Preact
This release brings some small improvements:
Link
can now accept a function in className
for applying styles to currently active links. Read more #419ssrPath
, e.g. pass /home?a=b
to ssrPath
and it will pre-fill ssrSearch
#420href
rendered when used with hash location. Before the fix, a link pointing to "/#users
was rendered as <a href="/users" />
without a hash. This now can be controlled via an hrefs
function defined on a router, but for built-in useHashLocation
this is done automatically. See #421flatMap()
method to flat child element in a switch. Thanks to abjfdi. See #254wouter
/wouter-preact
packages, because of the migration to monorepo in v3. This resulted in missing documentation on npmjs.org. This release fixes that.Over the past four months, @jeetiss and I (@molefrog) have been working on the upcoming major release v3. We've carefully listened to your requests on features that you wanted to see in Wouter and used this feedback to design the new version’s architecture and API.
But everything comes with a cost, so now, the library is no longer 1.5Kb... it's 2.1Kb. But it's packed with tons of new features: nested routing, wildcard patterns, useSearch
compatible with SSR, hash location, memory location, history state, useParams
, more secure History patching, improved TypeScript types, and more! We have also improved tooling to make the development process faster and more bulletproof: replaced Jest with Vitest, added dozens of new test cases, improved the speed of type linting, and simplified the Preact build by introducing a monorepo.
Below are some breaking changes that we tried to provide a migration guide for. To start using wouter v3, simply install:
> npm i [email protected]
Previously, there was a hacky workaround to get nested routing working. You would have to manually inherit the router from the parent and customise the base
path. This was far from ideal, and it only worked for static paths.
Now, nesting is a core feature of wouter and can be enabled on a route via the nest
prop. 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:
<Route path="/app" nest>
<Route path="/users/:id" nest>
<Route path="/orders" />
</Route>
</Route>
/app
, this is equivalent to having a base path in your app./app/user/1
, /app/user/1/anything
and so on./app/users/1/orders
. The match is strict, since that route does not have a nest
prop and it works as usual.If you call useLocation()
inside the last route, it will return /orders
and 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:
<Route path="/payments" nest>
<Route path="/all">
<Link to="~/home">Back to Home</Link>
</Route>
</Route>
useSearch
hook and history state supportuseSearch
hook is now part of the core. This means that you can import it from the main module, and will work with SSR too!
import { useSearch } from "wouter"
// returns "tab=settings&id=1"
// the hook for extracting search parameters is coming soon!
const searchString = useSearch()
<Router ssrSearch={request.search}>
{/* SSR! */}
</Router>
The long-awaited support for history state is also available now.
import { useLocation } from "wouter"
const [location, navigate] = useLocation()
navigate("/", { state: { user: 1 } });
// it works with Link and Redirect too!
<Link to="/" state={{ user: 1 }} />
// subscribing to state updates
import { useHistoryState } from "wouter/use-browser-location"
// this will only re-render when the actual state changes, not the URL
const state = useHistoryState()
In this release, we are retiring our homemade route pattern parser in favour of regexparam, the optimised 394B library for transforming route patterns into regular expressions. Although, this change might break existing apps (please refer to the list of breaking changes below), it unlocks some nice features that would not have been possible to achieve in v2:
// matches literally anything
<Route path="*" />
<Route path="/books/*/:genre" />
// use optional wildcards to make the trailing slash optional
<Route path="/app/*?" />
// Parameter w/ Suffix
<Route path="/movies/:title.(mp4|mov)" />
useBrowserLocation
and useHashLocation
hooksIn v2, in order to access the low-level location hook you had to import it from "wouter/use-location"
. The name of this file was confusing for many users, because of another function:
// V2 API
// returns current location scoped to the parent <Router hook={...} />
import { useLocation } from "wouter"
// low-level location hook, subscribes to current `location.pathname`
// these functions are not the same!
import useLocation from "wouter/use-location"
Unless you are writing an app that only subscribes to the current location and doesn't use other features such as routes, switches, params, base paths etc., you should use the later one.
We have renamed this module to "wouter/use-browser-location"
and added the second essential hook "wouter/use-hash-location"
for hash-based routing, so you won't need to reimplement it in your apps every time:
import { useHashLocation } from "wouter/use-hash-location"
<Router hook={useHashLocation} />
memoryLocation
for testing and in-memory routingThe package no longer ships with "wouter/static-location"
module, since this function wasn't well suited for SSR. Instead, there is now a new high-order hook called memoryLocation
which can be used for in-memory routing or for testing.
import { memoryLocation } from "wouter/memory-location"
// in-memory router
const { hook } = memoryLocation() // initial path is "/" by default
<Router hook={hook} />
// `static` option makes it immutable, use in testing environments that don't support `location`
const { hook, navigate } = memoryLocation({ path: "/dashboard", static: true })
navigate("/users") // nothing happens
// `record` option for keeping the navigation history in `history` array
const { hook, navigate, history } = memoryLocation({ record: true })
asChild
propOne of the notable changes is the behaviour of links that customise existing components or <a />
elements provided in children. We have thoroughly researched best practises from Next.js and Radix UI, and have decided to make link composition more explicit.
Now, Link
will always wrap its children in an <a />
tag, unless asChild
prop is provided:
// this no longer works! you will end up with double <a> elements
<Link to="/">
<a className="link">Home</a>
</Link>
// use this instead
<Link to="/" asChild>
<a className="link">Home</a>
</Link>
// this will still work as before
<Link to="/">Home</Link>
useRoute("/:category/:page/*")
will automatically infer the type of route parameters./:wild*
use /*
or *
. The name of this parameter is always "*"
/app/*?
will match /app
, /app/
and /app/foo/bar
/:user+
are no longer supported"wouter/static-location"
has been removed, use memoryLocation
instead (see above)"wouter/use-location"
module has been renamed to "wouter/use-browser-location"
(see above)Router
no longer accepts matcher
prop. Use parser
props instead. Module '"wouter/matcher"` has been removed.Route
component can no longer use scope-bypassing absolute paths, e.g. <Route path="~/home" />
will no longer work.Router
will now always inherit base path, parser and other options from the parent router, hence parent
prop has been removed. Except for the hook
prop, which will case all other options to reset. This is done to achieve better isolation between nested apps that use different location subscriptions (e.g. hash-based router should not inherit base from the outer location-based router).events
export has been removed from "wouter/use-location"
(this module is now called "wouter/use-browser-location"
)Over the past four months, @jeetiss and I (@molefrog) have been working on the upcoming major release v3. We've carefully listened to your requests on features that you wanted to see in Wouter and used this feedback to design the new version’s architecture and API.
But everything comes with a cost, so now, the library is no longer 1.5Kb... it's 2.1Kb. But it's packed with tons of new features: nested routing, wildcard patterns, useSearch
compatible with SSR, hash location, memory location, history state, useParams
, more secure History patching, improved TypeScript types, and more! We have also improved tooling to make the development process faster and more bulletproof: replaced Jest with Vitest, added dozens of new test cases, improved the speed of type linting, and simplified the Preact build by introducing a monorepo.
We encourage you to start testing v3 in your projects right away by installing this Release Candidate version. Below are some breaking changes that we tried to provide a migration guide for. To start using wouter v3, simply install:
> npm i wouter@next
Note. We've updated regexparam
dependency to the latest version, which ships with optional wildcards. If you've been using v3.0.0-rc.1
make sure you replace "wild"
parameter with "*"
.
Previously, there was a hacky workaround to get nested routing working. You would have to manually inherit the router from the parent and customise the base
path. This was far from ideal, and it only worked for static paths.
Now, nesting is a core feature of wouter and can be enabled on a route via the nest
prop. 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:
<Route path="/app" nest>
<Route path="/users/:id" nest>
<Route path="/orders" />
</Route>
</Route>
/app
, this is equivalent to having a base path in your app./app/user/1
, /app/user/1/anything
and so on./app/users/1/orders
. The match is strict, since that route does not have a nest
prop and it works as usual.If you call useLocation()
inside the last route, it will return /orders
and 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:
<Route path="/payments" nest>
<Route path="/all">
<Link to="~/home">Back to Home</Link>
</Route>
</Route>
useSearch
hook and history state supportuseSearch
hook is now part of the core. This means that you can import it from the main module, and will work with SSR too!
import { useSearch } from "wouter"
// returns "tab=settings&id=1"
// the hook for extracting search parameters is coming soon!
const searchString = useSearch()
<Router ssrSearch={request.search}>
{/* SSR! */}
</Router>
The long-awaited support for history state is also available now.
import { useLocation } from "wouter"
const [location, navigate] = useLocation()
navigate("/", { state: { user: 1 } });
// it works with Link and Redirect too!
<Link to="/" state={{ user: 1 }} />
// subscribing to state updates
import { useHistoryState } from "wouter/use-browser-location"
// this will only re-render when the actual state changes, not the URL
const state = useHistoryState()
In this release, we are retiring our homemade route pattern parser in favour of regexparam, the optimised 394B library for transforming route patterns into regular expressions. Although, this change might break existing apps (please refer to the list of breaking changes below), it unlocks some nice features that would not have been possible to achieve in v2:
// matches literally anything
<Route path="*" />
<Route path="/books/*/:genre" />
// use optional wildcards to make the trailing slash optional
<Route path="/app/*?" />
// Parameter w/ Suffix
<Route path="/movies/:title.(mp4|mov)" />
useBrowserLocation
and useHashLocation
hooksIn v2, in order to access the low-level location hook you had to import it from "wouter/use-location"
. The name of this file was confusing for many users, because of another function:
// V2 API
// returns current location scoped to the parent <Router hook={...} />
import { useLocation } from "wouter"
// low-level location hook, subscribes to current `location.pathname`
// these functions are not the same!
import useLocation from "wouter/use-location"
Unless you are writing an app that only subscribes to the current location and doesn't use other features such as routes, switches, params, base paths etc., you should use the later one.
We have renamed this module to "wouter/use-browser-location"
and added the second essential hook "wouter/use-hash-location"
for hash-based routing, so you won't need to reimplement it in your apps every time:
import { useHashLocation } from "wouter/use-hash-location"
<Router hook={useHashLocation} />
memoryLocation
for testing and in-memory routingThe package no longer ships with "wouter/static-location"
module, since this function wasn't well suited for SSR. Instead, there is now a new high-order hook called memoryLocation
which can be used for in-memory routing or for testing.
import { memoryLocation } from "wouter/memory-location"
// in-memory router
const { hook } = memoryLocation() // initial path is "/" by default
<Router hook={hook} />
// `static` option makes it immutable, use in testing environments that don't support `location`
const { hook, navigate } = memoryLocation({ path: "/dashboard", static: true })
navigate("/users") // nothing happens
// `record` option for keeping the navigation history in `history` array
const { hook, navigate, history } = memoryLocation({ record: true })
asChild
propOne of the notable changes is the behaviour of links that customise existing components or <a />
elements provided in children. We have thoroughly researched best practises from Next.js and Radix UI, and have decided to make link composition more explicit.
Now, Link
will always wrap its children in an <a />
tag, unless asChild
prop is provided:
// this no longer works! you will end up with double <a> elements
<Link to="/">
<a className="link">Home</a>
</Link>
// use this instead
<Link to="/" asChild>
<a className="link">Home</a>
</Link>
// this will still work as before
<Link to="/">Home</Link>
useRoute("/:category/:page/*")
will automatically infer the type of route parameters./:wild*
use /*
or *
. The name of this parameter is always "*"
/app/*?
will match /app
, /app/
and /app/foo/bar
/:user+
are no longer supported"wouter/static-location"
has been removed, use memoryLocation
instead (see above)"wouter/use-location"
module has been renamed to "wouter/use-browser-location"
(see above)Router
no longer accepts matcher
prop. Use parser
props instead. Module '"wouter/matcher"` has been removed.Route
component can no longer use absolute paths. <Route path="~/home" />
will no longer work.Router
will now always inherit base path, parser and other options from the parent router, hence parent
prop has been removed. Except for the hook
prop, which will case all other options to reset. This is done to achieve better isolation between nested apps that use different location subscriptions (e.g. hash-based router should not inherit base from the outer location-based router).events
export has been removed from "wouter/use-location"
(this module is now called "wouter/use-browser-location"
)Over the past four months, @jeetiss and I (@molefrog) have been working on the upcoming major release v3. We've carefully listened to your requests on features that you wanted to see in Wouter and used this feedback to design the new version’s architecture and API.
But everything comes with a cost, so now, the library is no longer 1.5Kb... it's 2.1Kb. But it's packed with tons of new features: nested routing, wildcard patterns, useSearch
compatible with SSR, hash location, memory location, history state, useParams
, more secure History patching, improved TypeScript types, and more! We have also improved tooling to make the development process faster and more bulletproof: replaced Jest with Vitest, added dozens of new test cases, improved the speed of type linting, and simplified the Preact build by introducing a monorepo.
We encourage you to start testing v3 in your projects right away by installing this Release Candidate version. There are some breaking changes that we tried to provide a migration guide for.
Previously, there was a hacky workaround to get nested routing working. You would have to manually inherit the router from the parent and customise the base
path. This was far from ideal, and it only worked for static paths.
Now, nesting is a core feature of wouter and can be enabled on a route via the nest
prop. 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:
<Route path="/app" nest>
<Route path="/users/:id" nest>
<Route path="/orders" />
</Route>
</Route>
/app
, this is equivalent to having a base path in your app./app/user/1
, /app/user/1/anything
and so on./app/users/1/orders
. The match is strict, since that route does not have a nest
prop and it works as usual.If you call useLocation()
inside the last route, it will return /orders
and 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:
<Route path="/payments" nest>
<Route path="/all">
<Link to="~/home">Back to Home</Link>
</Route>
</Route>
useSearch
hook and history state supportuseSearch
hook is now part of the core. This means that you can import it from the main module, and will work with SSR too!
import { useSearch } from "wouter"
// returns "tab=settings&id=1"
// the hook for extracting search parameters is coming soon!
const searchString = useSearch()
<Router ssrSearch={request.search}>
{/* SSR! */}
</Router>
The long-awaited support for history state is also available now.
import { useLocation } from "wouter"
const [location, navigate] = useLocation()
navigate("/", { state: { user: 1 } });
// it works with Link and Redirect too!
<Link to="/" state={{ user: 1 }} />
// subscribing to state updates
import { useHistoryState } from "wouter/use-browser-location"
// this will only re-render when the actual state changes, not the URL
const state = useHistoryState()
In this release, we are retiring our homemade route pattern parser in favour of regexparam, the optimised 394B library for transforming route patterns into regular expressions. Although, this change might break existing apps (please refer to the list of breaking changes below), it unlocks some nice features that would not have been possible to achieve in v2:
// matches literally anything
<Route path="*" />
<Route path="/books/*/:genre" />
// use optional wildcards to make the trailing slash optional
<Route path="/app/*?" />
// Parameter w/ Suffix
<Route path="/movies/:title.(mp4|mov)" />
useBrowserLocation
and useHashLocation
hooksIn v2, in order to access the low-level location hook you had to import it from "wouter/use-location"
. The name of this file was confusing for many users, because of another function:
// V2 API
// returns current location scoped to the parent <Router hook={...} />
import { useLocation } from "wouter"
// low-level location hook, subscribes to current `location.pathname`
// these functions are not the same!
import useLocation from "wouter/use-location"
Unless you are writing an app that only subscribes to the current location and doesn't use other features such as routes, switches, params, base paths etc., you should use the later one.
We have renamed this module to "wouter/use-browser-location"
and added the second essential hook "wouter/use-hash-location"
for hash-based routing, so you won't need to reimplement it in your apps every time:
import { useHashLocation } from "wouter/use-hash-location"
<Router hook={useHashLocation} />
memoryLocation
for testing and in-memory routingThe package no longer ships with "wouter/static-location"
module, since this function wasn't well suited for SSR. Instead, there is now a new high-order hook called memoryLocation
which can be used for in-memory routing or for testing.
import { memoryLocation } from "wouter/memory-location"
// in-memory router
const { hook } = memoryLocation() // initial path is "/" by default
<Router hook={hook} />
// `static` option makes it immutable, use in testing environments that don't support `location`
const { hook, navigate } = memoryLocation({ path: "/dashboard", static: true })
navigate("/users") // nothing happens
// `record` option for keeping the navigation history in `history` array
const { hook, navigate, history } = memoryLocation({ record: true })
asChild
propOne of the notable changes is the behaviour of links that customise existing components or <a />
elements provided in children. We have thoroughly researched best practises from Next.js and Radix UI, and have decided to make link composition more explicit.
Now, Link
will always wrap its children in an <a />
tag, unless asChild
prop is provided:
// this no longer works! you will end up with double <a> elements
<Link to="/">
<a className="link">Home</a>
</Link>
// use this instead
<Link to="/" asChild>
<a className="link">Home</a>
</Link>
// this will still work as before
<Link to="/">Home</Link>
useRoute("/:category/:page/*")
will automatically infer the type of route parameters./:wild*
use /*
or *
. The name of this parameter is always "wild"
/app/*?
will match /app
, /app/
and /app/foo/bar
/:user+
are no longer supported"wouter/static-location"
has been removed, use memoryLocation
instead (see above)"wouter/use-location"
module has been renamed to "wouter/use-browser-location"
(see above)Router
no longer accepts matcher
prop. Use parser
props instead. Module '"wouter/matcher"` has been removed.Route
component can no longer use absolute paths. <Route path="~/home" />
will no longer work.Router
will now always inherit base path, parser and other options from the parent router, hence parent
prop has been removed. Except for the hook
prop, which will case all other options to reset. This is done to achieve better isolation between nested apps that use different location subscriptions (e.g. hash-based router should not inherit base from the outer location-based router).events
export has been removed from "wouter/use-location"
(this module is now called "wouter/use-browser-location"
)In this version, we are introducing a new prop that you can pass to the top-level Router component: ssrPath
. The migration to the useSyncExternalStore
hook in v2.10.0 made it possible for us to use a native way of telling React what the location should be when rendering on the server.
Prior to this release, our users had to override the default location hook with wouter/static-location
, which lacked a nice DX and could cause hydration warnings. We are deprecating the static location hook in favor of the new ssrPath
prop. Rendering your app on the server is now as easy as:
const handleRequest = (req, res) => {
// top-level Router is mandatory in SSR mode
const prerendered = renderToString(
<Router ssrPath={req.path}>
<App />
</Router>
);
// respond with prerendered html
};
You can find a detailed guide in the README. To see the new API in action, we have prepared a simple demo powered by Wouter and Ultra, a server-side rendering framework for Deno. Take a look at how the app is rendered on the server and then hydrated in the browser.
Full changelog:
index.d.ts
now doesn't export types of methods that aren't present in the module #306 Thanks @Mati365wouter-preact
: Preact type declarations are now up-to-date with the main package, type exports have been fixed #309 #294 Thanks @robertknight and @jerssonMThis release fixes missing export error caused by incorrect import of useInsertionEffect
hook in React <18. See https://github.com/molefrog/wouter/issues/292
Thanks to @hudochenkov!
Other changes:
In this alpha release, we're migrating useLocation
hook to useSyncExternalStore
under the hood (the new API for subscribing to external state sources compatible for concurrent mode).
This hook is available in React 18, for all earlier versions we've included a shim. We've also done some heavy refactoring which should lead to better performance and new features such as:
useSearch
exported from wouter/use-location
for subscribing to location.search
useLocationProperty
for subscribing to arbitrary location updates.navigate
with stable reference and no dependency on current location, which means that your components that only perform navigation won't have unnecessary re-renders.import { useSearch, useLocationProperty, navigate } from 'wouter/use-location';
// get all search params:
const search = useSearch();
// set all search params:
navigate("?name=john");
// get individual search param (will not trigger a re-render if other params change! 🎉 )
const nameValue = useLocationProperty(() => new URLSearchParams(window.location.search).get("name"));
// set individual search param:
const params = new URLSearchParams(window.location.search);
params.set("name", "john");
navigate("?" + params.toString());
Thanks @HansBrende for their contribution.
In this alpha release, we're migrating useLocation
hook to useSyncExternalStore
under the hood (the new API for subscribing to external state sources compatible for concurrent mode).
This hook is available in React 18, for all earlier versions we've included a shim. We've also done some heavy refactoring which should lead to better performance and new features such as:
useSearch
exported from wouter/use-location
for subscribing to location.search
useLocationProperty
for subscribing to arbitrary location updates.navigate
with stable reference and no dependency on current location, which means that your components that only perform navigation won't have unnecessary re-renders.import { useSearch, useLocationProperty, navigate } from 'wouter/use-location';
// get all search params:
const search = useSearch();
// set all search params:
navigate("?name=john");
// get individual search param (will not trigger a re-render if other params change! 🎉 )
const nameValue = useLocationProperty(() => new URLSearchParams(window.location.search).get("name"));
// set individual search param:
const params = new URLSearchParams(window.location.search);
params.set("name", "john");
navigate("?" + params.toString());
Thanks @HansBrende for their contribution.
parent
prop in Router
, #270