Routes

Routes are JavaScript objects that describe valid locations within an application.

Route Properties

Routes have two required properties—name and path—and a number of optional properties.

Route path

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.

let routes = prepareRoutes([
  {
    path: "" // matches the root "/"
  },
  {
    // the "id" segment can be any value
    path: "a/:id"
  }
]);

Route names

A route's name is a unique identifier for a route.

let routes = prepareRoutes([
  {
    name: "Home",
    path: ""
  },
  {
    name: "Album",
    // the "id" segment can be any value
    path: "a/:id"
  }
]);

The uniqueness of names is important so that you can interact with routes. The router's route method is used for getting data about a route using its name. Curi also has a concept of route interactions for working with this route data. For example, Curi provides a pathname route interaction to generate the pathname of a URL. This is particularly useful for routes with path params, since the pathname interaction will automatically insert these for you.

let route = router.route("Album");
let path = pathname(route, { 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 a function that runs asynchronous actions and returns a Promise. A response will not be emitted until after a route's resolve function has resolved.

{
  name: "User",
  path: "u/:id",
  resolve({ params }) {
    // run code to verify the user can view the page
    let authorized = Promise.resolve(true);

    // import the User component using the import() API
    let body = import("./components/User");

    // get specific data using the route's params
    let data = UserAPI.get(params.id);
    return Promise.all([ authorized, body, data ]);
  }
}

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 respond function. When a route matches, a response object with "match" properties is generated. An object returned by the respond function gets merged with the match response object*. The responses guide covers all of the response properties.

* Only valid properties are merged.

The respond function receives an object with a number of properties.

  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 the resolve function has an uncaught error.
import User from "./components/User";

let routes = prepareRoutes([
  {
    name: "User",
    path: "u/:id",
    resolve({ params }) {
      return UserAPI.get(params.id);
    },
    respond({ match, resolved, error }) {
      if (error) {
        // ...
      }
      return {
        body: User,
        data: resolved.data,
        title: `User ${match.params.id}`
      };
    }
  }
]);

Matching Routes

When you create a router, you pass an array of all of the valid routes for the application. Whenever Curi receives a new location, it matches the new location's pathname against the valid routes to determine which one matches.

Route matching tests the route objects in the order that they are defined in the array. If a route partially matches (i.e. it matches the beginning of the pathname, but there are leftover unmatched segments of the pathname), and it has children routes, those will be checked before moving to the route's next sibling.

let routes = prepareRoutes([
  { name: "One", path: "one" },
  { name: "Two", path: "two", children: [
    { name: "And a half", path: "point-five" },
    // matches /two/point-five
  ]},
  { name: "Three", path: "three" },
]);

// route match order:
// 1. One
// 2. Two
// 3. And a half (only if Two partially matches)
// 4. Three

No Matching Route

If none of your routes match a location, Curi will do nothing! Your routes should include a "catch all" route to match these locations. The path "(.*)" matches every pathname, so using that as the path of the last route will catch every location.

{
  name: 'Not Found',
  path: '(.*)',
}

Route Matching Walkthrough

let routes = prepareRoutes([
  {
    name: 'Home',
    path: '',
  },
  {
    name: 'Album',
    path: 'a/:album'
  },
  {
    name: 'Not Found',
    path: '(.*)'
  }
]);

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.

let routes = prepareRoutes([
  {
    name: 'Home',
    path: '',
  },
  {
    name: 'Album',
    path: 'a/:album',
    children: [
      {
        name: 'Song',
        path: ':title'
      }
    ]
  },
  {
    name: 'Not Found',
    path: '(.*)'
  }
]);

If the pathname is '/a/Coloring+Book/All+Night', the Album route will partially match the pathname ("/a/Coloring+Book"). That route has children routes, so the unmatched segments ("/All+Night") will be checked against those routes. The Song route matches the remaining segments, so the router matches the Song route to the location.

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: {
    parse: {
      end: false
    }
  }
}

Preparing Routes

The routes array should be wrapped in a prepareRoutes call. This will pre-build the routes for the router, which is especially useful for server rendering, where a new router is created for every request.

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

// plain routes
let routes = [...]

// prepared routes
export default prepareRoutes(routes);