Routes & Responses
Routes describe the valid locations within an application. Responses provide data about the route that matches the current location.
Responses#
When Curi receives a location, it compares the location's pathname
to each route's path
to find which one matches best and uses that route to create a response object.
The Properties of a Response Object#
There are two types of response properties.
The "match" properties are set based on the route that matches a location. A response always has these properties.
// match properties
{
// The location object used to generate the response.
location: { pathname: '/photos/6789/12345', ... },
// The name of the best matching route
name: 'Photo',
// The name of ancestor routes that matched
// part of the location's pathname
partials: ['Album'],
// An object containing the values parsed
// from the pathname by path-to-regexp.
// This includes params from ancestor routes.
params: { photoID: 12345, albumID: 6789 },
}
The "settable" properties are ones that are added by a matched route's response()
function. These only exist on the response when they are returned by a route's response()
function.
The "settable" properties are:
property | description |
---|---|
body | The component(s) that should be rendered for a route. |
status | An http status, mostly useful for server side rendering. |
data | A place to attach any data you want to the response, such as data loaded in the route's resolve functions. |
title | The response's title, which can be used with @curi/side-effect-title to set the browsers tab's title. |
error | A convenient place to attach any errors to the response. |
redirectTo | An object describing a route that Curi should automatically redirect to. |
// settable properties (optional)
{
body: Photo,
// or maybe
body: {
menu: PhotoMenu,
main: Photo
},
// Please see below for more information
// about this property
status: 200,
data: {...},
title: 'Photo 12345',
error: undefined,
redirectTo: {...}
}
Response Body#
Curi isn't strict about how you use responses, but you will most likely always want to use a route's response()
function to attach a body
property to a response. The usual pattern is to use a route's body
property to describe which component(s) to render when a route matches. This can either be a single component for basic layouts or an object with a number of components for advanced layouts.
body
"shape". If one route returns a single component while another route return an object, you will be making rendering more complicated for yourself.// do NOT do this
// mixing body shapes complicates rendering
const routes = [
{
response() {
return { body: One }
}
},
{
response() {
return {
body: {
main: Main,
menu: Menu
}
}
}
}
];
Redirect Response#
When a route's response()
function returns an object with a redirectTo
property, the router will use it to generate a location object that Curi will automatically redirect to.
{
// The redirectTo property provides information on
// where you should redirect to
redirectTo: { pathname: '/login' }
}
You can choose whether or not you want responses with a redirectTo
property to be emitted. If they are not emitted, then the router will redirect without the application's observers knowing about the redirect. The default behavior is to emit redirects, but this also means that you have to render using the redirect response. The { emitRedirects: false }
option prevents this.
const router = curi(history, routes, {
emitRedirects: false
});
Routes#
Routes are JavaScript objects with two required properties—name
and path
—and a number of optional properties.
A route's path
is used to determine if a route matches a location. Path strings use path-to-regexp
formatting, which allows you to define dynamic path parameters that a route should match.
A route's name
is a unique identifier for a route. The name
is used to interact with a specific route.
const routes = [
{
name: "Home",
path: ""
},
{
name: "Album",
// the "id" segment can be any value
path: "a/:id"
}
];
Resolve#
When a route matches, you might want to perform some actions before the application re-renders. This can include validating that a user is authorized to navigate to a route and loading data based on the path parameters parsed from the location.
A route's resolve
property is an optional object for attaching functions to a route. A response will not be emitted until after all of a route's resolve
functions have finished.
A route with resolve
properties is asynchronous, which has effects to be aware of. You can read about these in the Sync or Async guide.
Curi uses Promises to manage a route's resolve
functions. Each function should return a Promise. This makes it easy to wait for all of the resolve
functions to complete before emitting the response for a matched route.
Promise.resolve()
can be used to return a Promise.When resolve
functions are called, they will be passed an object with the "match" properties of a response. These are the matched route's name
, the location
, an object of parsed params
, and an array of the names of partial
route matches.
{
name: "User",
path: "u/:id",
resolve: {
authorized: () => {
// run code to verify the user can view the page
return Promise.resolve(true);
},
body: () => {
// import the User component using the import() API
return import("./components/User");
},
data: ({ name, location, params, partials }) => {
// get specific data using the route's params
return UserAPI.get(params.id);
}
}
}
The Response Function#
Each route can have a response()
function. The role of the response()
function is to return an object of properties to merge onto the response object that will be emitted for the new location.
Only valid response properties will be merged onto the response. These are the optional response properties listed above (body
, title
, status
, data
, redirectTo
, and error
).
The function receives an object with a number of properties you might find useful.
The first is an object of resolve
properties (the base response properties).
The second is a resolved
object, which contains the resolved values from the route's resolve
functions.
The third property is an error
, which is only defined if one of the resolve
functions throws an error and you don't catch it.
import User from "./components/User";
const routes = [
{
name: "User",
path: "u/:id",
resolve: {
data: ({ params }) => UserAPI.get(params.id)
},
response({ match, resolved, error }) {
if (error) {
// ...
}
return {
body: User,
data: resolved.data,
title: `User ${match.params.id}`
};
}
}
];
Matching Routes#
Whenever Curi receives a new location, it will determine which route has a path
that matches the new location's pathname
by walking over the route objects in the order that they are defined in the array. If a route has children
, those will be checked before moving to the route's nest sibling.
We'll use this simple route setup to demonstrate how this works.
const routes = [
{
name: 'Home',
path: '',
},
{
name: 'Album',
path: 'a/:album'
},
{
name: 'Not Found',
path: '(.*)' // this matches EVERY pathname
}
];
Curi's default matching behavior looks for exact matches. This means that when the route only matches part of the pathname, it does not count as a match. If the user navigates to a location with the pathname "/a/red/yellow"
, the Album
route will only partially match, so Curi will move on to the next route, Not Found
, which has a catch all path
that matches every pathname. Routes can be configured to allow partial matching, but exact matching is usually preferable.
If a route has children, Curi will check if any of those routes form a complete match before moving on to the next route in the routes array.
// when the pathname is '/a/Coloring+Book/All+Night',
// the Album route will partially match the pathname.
// Then, its child route Song will be tested and fully
// match the pathname.
{
name: 'Album',
path: 'a/:album',
children: [
{
name: 'Song',
path: ':title'
}
]
}
You can control whether a route does exact or partial matching with pathOptions
property. If you set { end: false }
, a route that partially matches will consider itself matched.
// when the pathname is
// '/a/Good+Kid,+M.A.A.D+City/Poetic+Justice',
// the Album route will partially match, but because
// it sets "end" to false, the partial match will still be used.
{
name: 'Album',
path: 'a/:albumID',
pathOptions: {
end: false
}
}
No Matching Route#
If none of your routes match a location, Curi will do nothing! You need to set a catch-all route to match these locations yourself. The best way to do this is to add a route to the end of your routes array with a path
of "(.*)"
, which will match every pathname.
{
name: 'Not Found',
path: '(.*)',
}