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.
The router is designed to list all routes in a single file, with limited nesting. We prefer this design, as it makes it very easy to track which routes map to which pages.
Router and Route
The first thing you need is a Router
. It will contain all of your routes. The router will attempt to match the current URL to each route in turn, and only render those with a matching path
. 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.
notfound
route can't be nested in a Set
If you want to wrap your custom notfound page in a Layout
, then you should add the Layout
to the page instead. See customizing the NotFoundPage.
Each route is specified with a Route
. Our first route will tell the router what to render when no other route matches:
import { Router, Route } from '@cedarjs/router'
const Routes = () => (
<Router>
<Route notfound page={NotFoundPage} />
</Router>
)
export default Routes
The router 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
:
<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.
Private Routes
Some pages should only be visible to authenticated users. We support this using the PrivateSet
component. Read more further down.
Redirect Routes
If you move a page you might still want to keep the old route around, so that
old links to your site keep working. To this end CedarJS supports the
redirect
prop on routes, which allows you to specify the name of the route
you want to redirect to:
<Route path="/blog/{id}" redirect="post" />
<Route path="/posts/{id}" page="PostPage" name="post" />
When doing redirects the original path parameters are also passed to the page
the user is redirected to. So, in the example above, if a user goes to
/blog/5
they will be redirected to /posts/5
.
For redirect routes the name
prop is optional. If you want to be able to keep
using old route names in your code you can keep the name around. If you want to
update them all you can remove the name prop and you'll get TypeScript errors
everywhere it's used. You can also decide to reuse the name for your new route,
and all existing links in your code will continue to just work.
If you prefer, you can also specify the path of the route you want to redirect to:
<Route path="/blog/{id}" redirect="/posts/{id}" />
<Route path="/posts/{id}" page="PostPage" name="post" />
Sets of Routes
You can group Routes into sets using the Set
component. Set
allows you to wrap a set of Routes in another component or array of components—usually a Context, a Layout, or both:
import { Router, Route, Set } from '@cedarjs/router'
import BlogContext from 'src/contexts/BlogContext'
import BlogLayout from 'src/layouts/BlogLayout'
const Routes = () => {
return (
<Router>
<Set wrap={[BlogContext, BlogLayout]}>
<Route path="/" page={HomePage} name="home" />
<Route path="/about" page={AboutPage} name="about" />
<Route path="/contact" page={ContactPage} name="contact" />
<Route path="/blog-post/{id:Int}" page={BlogPostPage} name="blogPost" />
</Set>
</Router>
)
}
export default Routes
The wrap
prop accepts a single component or an array of components. Components are rendered in the same order they're passed, so in the example above, Set expands to:
<BlogContext>
<BlogLayout>
<Route path="/" page={HomePage} name="home" />
// ...
</BlogLayout>
</BlogContext>
Conceptually, this fits with how we think about Context and Layouts as things that wrap Pages and contain content that’s outside the scope of the Pages themselves. Crucially, since they're higher in the tree, BlogContext
and BlogLayout
won't rerender across Pages in the same Set.
There's a lot of flexibility here. You can even nest Sets
to great effect:
import { Router, Route, Set } from '@cedarjs/router'
import BlogContext from 'src/contexts/BlogContext'
import BlogLayout from 'src/layouts/BlogLayout'
import BlogNavLayout from 'src/layouts/BlogNavLayout'
const Routes = () => {
return (
<Router>
<Set wrap={[BlogContext, BlogLayout]}>
<Route path="/" page={HomePage} name="home" />
<Route path="/about" page={AboutPage} name="about" />
<Route path="/contact" page={ContactPage} name="contact" />
<Set wrap={BlogNavLayout}>
<Route path="/blog-post/{id:Int}" page={BlogPostPage} name="blogPost" />
</Set>
</Set>
</Router>
)
}
Forwarding props
All props you give to <Set>
(except for wrap
) will be passed to the wrapper components.
So this...
<Set wrap={MainLayout} theme="dark">
<Route path="/" page={HomePage} name="home" />
</Set>
becomes...
<MainLayout theme="dark">
<Route path="/" page={HomePage} name="home" />
</MainLayout>
PrivateSet
A PrivateSet
makes all Routes inside that Set require authentication. When a user isn't authenticated and attempts to visit one of the Routes in the PrivateSet
, they'll be redirected to the Route passed as the PrivateSet
's unauthenticated
prop. The originally-requested Route's path is added to the query string as a redirectTo
param. This lets you send the user to the page they originally requested once they're logged-in.
Here's an example of how you'd use a PrivateSet
:
<Router useAuth={useAuth}>
<Route path="/" page={HomePage} name="home" />
<PrivateSet unauthenticated="home">
<Route path="/admin" page={AdminPage} name="admin" />
</PrivateSet>
</Router>
For more fine-grained control, you can specify roles
(which takes a string for a single role or an array of roles), and the router will check to see that the current user is authorized before giving them access to the Route. If they're not, they will be redirected to the page specified in the unauthenticated
prop, such as a "forbidden" page. Read more about Role-based Access Control in Redwood here.
To protect private routes for access by a single role:
<Router useAuth={useAuth}>
<PrivateSet unauthenticated="forbidden" roles="admin">
<Route path="/admin/users" page={UsersPage} name="users" />
</PrivateSet>
<Route path="/forbidden" page={ForbiddenPage} name="forbidden" />
</Router>
To protect private routes for access by multiple roles:
<Router useAuth={useAuth}>
<PrivateSet unauthenticated="forbidden" roles={['admin', 'editor', 'publisher']}>
<Route path="/admin/posts/{id:Int}/edit" page={EditPostPage} name="editPost" />
</PrivateSet>
<Route path="/forbidden" page={ForbiddenPage} name="forbidden" />
</Router>
A route is permitted when authenticated and user has any of the provided roles such as "admin"
or ["admin", "editor", "publisher"]
.
Redwood uses the useAuth
hook under the hood to determine if the user is authenticated. Read more about authentication in Redwood here.
Link and named route functions
When it comes to routing, matching URLs to Pages is only half the equation. The other half is generating links to your pages. The router 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):
import { Link, routes } from '@cedarjs/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 Link
s to it (as long as you keep the name
the same)!