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.

Note: path strings do not start with a slash.
const routes = prepareRoutes([
  {
    name: "Home",
    path: ""
  },
  {
    name: "Album",
    // the "id" segment can be any value
    path: "a/:id"
  }
]);

Preparing Routes

The routes array should be wrapped in a prepareRoutes() call. This will pre-build the routes for the router.

import { prepareRoutes } from "@curi/router";

// plain routes
const routes = [...]

// prepared routes
export default prepareRoutes(routes);

Route names

Why do routes have names? Curi lets you interact with routes using their names.

For example, Curi provides a pathname route interaction to generate the pathname of a location to navigate to. Instead of manually writing pathname strings, you tell Curi the name of the route that you want to navigate to (and also any required params) and Curi will create the pathname for you.

const pathname = router.route.pathname(
  "Album", { id: "abcd" }
);
// pathname = "/a/abcd"

Asynchronous Routes

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.

{
  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);
    }
  }
}

A route with resolve properties is asynchronous, which has some effects to be aware of. You can read about these in the Sync or Async guide.

The Response Function

Each route can have a response() function, which returns an object of properties to merge with route's "match" properties. This combined object is a "response" object.

import User from "./components/User";

const routes = prepareRoutes([
  {
    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}`
      };
    }
  }
]);

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.

  1. match is an object of properties based on the matched route.
  2. resolved is an object with the results of the route's resolve functions.
  3. error is only defined when one of the resolve functions throws an error does not catch it itself.

Matching Routes

Whenever Curi receives a new location, it will determine which route has a path that matches the new location's pathname. It does this 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.

No Matching Route

Warning: If none of your routes match a location, Curi will do nothing! Your routes should include 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: '(.*)',
}

Route Matching Walkthrough

const routes = prepareRoutes([
  {
    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. Routes can be configured to allow partial matching, but exact matching is usually preferable.

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. Not Found has a catch all path that matches every pathname, so it will match the location.

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'
    }
  ]
}

Path Matching Options

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
  }
}