Redwood Router

This is the built-in router for Redwood apps. It takes inspiration from Ruby on Rails, React Router, and Reach Router, but is very opinionated in its own way.

WARNING: This software is in alpha and should not be considered suitable for production use. In the "make it work; make it right; make it fast" paradigm, RR is in the later stages of the "make it work" phase.

Redwood Router (RR from now on) is designed to list all routes in a single file, without any nesting. We prefer this design, as it makes it very easy to track which routes map to which pages.

Installation

RR was designed for use in Redwood apps, and if you use yarn create-redwood-app it will be installed for you. The rest of the documentation here will use examples that are appropriate in that context. That said, you can use RR outside of Redwood apps too! To learn more, see Installation and use outside of a Redwood app at the end of this document.

Router and Route

The first thing you need is a Router. It will contain all of your routes. RR will attempt to match the current URL to each route in turn, stopping when it finds a match, and rendering that route only. The only exception to this is the notfound route, which can be placed anywhere in the list and only matches when no other routes do.

Each route is specified with a Route. Our first route will tell RR what to render when no other route matches:

// Routes.js
import { Router, Route } from '@redwoodjs/router'

const Routes = () => (
  <Router>
    <Route notfound page={NotFoundPage} />
  </Router>
)

export default Routes

RR expects a single Route with a notfound prop. When no other route is found to match, the component in the page prop will be rendered.

To create a route to a normal Page, you'll pass three props: path, page, and name:

// Routes.js
<Route path="/" page={HomePage} name="home" />

The path prop specifies the URL path to match, starting with the beginning slash. The page prop specifies the Page component to render when the path is matched. The name prop is used to specify the name of the named route function.

When it comes to routing, matching URLs to Pages is only half the equation. The other half is generating links to your pages. RR makes this really simple without having to hardcode URL paths. In a Page component, you can do this (only relevant bits are shown in code samples from now on):

// SomePage.js
import { Link, routes } from '@redwoodjs/router'

// Given the route in the last section, this produces: <a href="/">
const SomePage = () => <Link to={routes.home()} />

You use a Link to generate a link to one of your routes and can access URL generators for any of your routes from the routes object. We call the functions on the routes object named route functions and they are named after whatever you specify in the name prop of the Route.

Named route functions simply return a string, so you can still pass in hardcoded strings to the to prop of the Link component, but using the proper named route function is easier and safer. Plus, if you ever decide to change the path of a route, you don't need to change any of the Links to it (as long as you keep the name the same)!

Route parameters

To match variable data in a path, you can use route parameters, which are specified by a parameter name surrounded by curly braces:

// Routes.js
<Route path="/user/{id}>" page={UserPage} name="user" />

This route will match URLs like /user/7 or /user/mojombo. You can have as many route parameters as you like:

// Routes.js
<Route path="/blog/{year}/{month}/{day}/{slug}" page={PostPage} name="post" />

By default, route parameters will match up to the next slash or end-of-string. Once extracted, the route parameters are sent as props to the Page component. In the 2nd example above, you can receive them like so:

// PostPage.js
const PostPage = ({ year, month, day, slug }) => { ... }

Named route functions with parameters

If a route has route parameters, then its named route function will take an object of those same parameters as an argument:

// SomePage.js
<Link to={routes.user({ id: 7 })} />

All parameters will be converted to strings before being inserted into the generated URL. If you don't like the default JavaScript behavior of how this conversion happens, make sure to convert to a string before passing it into the named route function.

Route parameter types

Route parameters are extracted as strings by default, but they will often represent typed data. RR offers a convenient way to auto-convert certain types right in the path specification:

// Routes.js
<Route path="/user/{id:Int}" page={UserPage} name="user" />

By adding :Int onto the route parameter, you are telling RR to only match /\d+/ and then use Number() to convert the parameter into a number. Now, instead of a string being sent to the Page, a number will be sent! This means you could have both a route that matches numeric user IDs and a route that matches string IDs:

// Routes.js
<Route path="/user/{id:Int}" page={UserIntPage} name="userInt" />
<Route path="/user/{id}" page={UserStringPage} name="userString" />

Now, if a request for /user/mojombo comes in, it will fail to match the first route, but will succeed in matching the second.

Core route parameter types

We call built-in parameter types core parameter types. All core parameter types begin with a capital letter. Here are the types:

  • Int - Matches and converts an integer.

User route parameter types

RR goes even further, allowing you to define your own route parameter types. Your custom types must begin with a lowercase letter. You can specify them like so:

// Routes.js
const userRouteParamTypes = {
  slug: {
    constraint: /\w+-\w+/,
    transform: (param) => param.split('-'),
  }
}

<Router paramTypes={userRouteParamTypes}>
  <Route path="/post/{name:slug}" page={PostPage} name={post} />
</Router>

Here we've created a custom slug route parameter type. It is defined by a constraint and a transform. Both are optional; the default constraint is /[^/]+/ and the default transform is (param) => param.

In the route we've specified a route parameter of {name:slug} which will invoke our custom route parameter type and if we have a requst for /post/redwood-router, the resulting name prop delivered to PostPage will be ['redwood', 'router'].

useParams

Sometimes it's convenient to receive route parameters as the props to the Page, but in the case where a deeply nested component needs access to the route parameters, it quickly becomes tedious to pass those props through every intervening component. RR solves this with the useParams hook:

// SomeDeeplyNestedComponent.js
import { useParams } from '@redwoodjs/router'

const SomeDeeplyNestedComponent = () => {
  const { id } = useParams()
  ...
}

In the above example, we've pulled in the id route parameter without needing to have it passed in to us from anywhere.

If you'd like to programmatically navigate to a different page, you can simply use the navigate function:

// SomePage.js
import { navigate, routes } from '@redwoodjs/router'

const SomePage = () => {
  const onSomeAction = () => {
    navigate(routes.home())
  }
  ...
}

Code-splitting

By default, RR (when used in a Redwood app) will code-split on every Page, creating a separate lazy-loaded webpack bundle for each. When navigating from page to page, RR will wait until the new Page module is loaded before re-rendering, thus preventing the "white-flash" effect.

Not code splitting

If you'd like to override the default lazy-loading behavior and include certain Pages in the main webpack bundle, you can simply add the import statement to the Routes.js file:

// Routes.js

import HomePage from 'src/pages/HomePage'

Redwood will detect your explicit import and refrain from splitting that page into a separate bundle. Be careful with this feature, as you can easily bloat the size of your main bundle to the point where your initial page load time becomes unacceptable.

PageLoadingContext

Because lazily-loaded pages can take a non-negligible amount of time to load (depending on bundle size and network connection), you may want to show a loading indicator to signal to the user that something is happening after they click a link. RR makes this really easy with usePageLoadingContext:

// SomeLayout.js

import { usePageLoadingContext } from '@redwoodjs/router'

const SomeLayout = (props) => {
  const { loading } = usePageLoadingContext()
  return (
    <div>
      {loading && <div>Loading...</div>}
      <main>{props.children}</main>
    </div>
  )
}

When the lazy-loaded page is loading, PageLoadingContext.Consumer will pass { loading: true } to the render function, or false otherwise. You can use this context wherever you like in your application!

After adding this to your app you will probably not see it when navigating between pages. This is because having a loading indicator is nice, but can get annoying when it shows up every single time you navigate to a new page. In fact, this behavior makes it feel like your pages take even longer to load than they actually do! RR takes this into account and, by default, will only show the loader when it takes more than 1000 milliseconds for the page to load. You can change this to whatever you like with the pageLoadingDelay prop on Router:

// Routes.js

<Router pageLoadingDelay={500}>...</Router>

Now the loader will show up after 500ms of load time. To see your loading indicator, you can set this value to 0 or, even better, change the network speed in developer tools to "Slow 3G" or another agonizingly slow connection speed.

Installation and use outside of a Redwood app

If you'd like to use RR in a non-Redwood app, you can! Start by installing it:

$ yarn add @redwoodjs/router

Then you can import and use the various RR components like normal. The only exception being that Redwood automatically takes care of making all your Pages available in the Routes.js file. When using RR outside that context, you'll need to do this on your own. By default, RR provides code splitting for every Page. To mimic this, you'll need to define each Page as an object, like so:

const HomePage = {
  name: 'HomePage',
  loader: () => import('path/to/HomePage.js'),
}

...

<Router>
  <Route path="/" page={HomePage} name="home" />
</Router>

Then RR will take care of the lazy loading for you. If you'd prefer to have some or all of your Pages included in the main webpack bundle, you can import them normally:

import HomePage from 'path/to/HomePage.js'

...

<Router>
  <Route path="/" page={HomePage} name="home" />
</Router>

That's it! Everything else should work the same as it does inside a Redwood app!