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:

propertydescription
bodyThe component(s) that should be rendered for a route.
statusAn http status, mostly useful for server side rendering.
dataA place to attach any data you want to the response, such as data loaded in the route's resolve functions.
titleThe response's title, which can be used with @curi/side-effect-title to set the browsers tab's title.
errorA convenient place to attach any errors to the response.
redirectToAn 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.

Note: Each route should use the same 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.

Note: 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#

Warning:

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: '(.*)',
}