@curi/router

Installation

The package can be installed through npm (you need to have Node & NPM installed).

npm install @curi/router

Prefer inline scripts? A full (.umd.js) and minified (.min.js) script is available for every version through Unpkg. You can access the package's exports through window.Curi.

About

The @curi/router package is used to create a router.

API

curi

The curi export is a function to create a router. It has two required arguments: a history object and a routes array, and an optional third argument: an options object.

import { curi } from '@curi/router';

const router = curi(history, routes, options);

Arguments

history

A Hickory history object that will power navigation within the application. The getting started guide provides more information on how to choose which history type is right for an application.

import Browser from "@hickory/browser";

const history = Browser();
const router = curi(history, routes);
routes

An array of prepared route objects describing all valid routes in the application.

const routes = prepareRoutes([
  { name: "Home", path: "" },
  { name: "About", path: "about" }
]);

const router = curi(history, routes);
options

An optional object with additional properties that can be passed to the router.

  • route - An array of route interactions. These are functions for interacting with routes based on their name.

    The pathname interaction is included by default; any other interactions are provided through this array.

    import active from "@curi/route-active";
    import ancestors from "@curi/route-ancestors";
    
    const routes = prepareRoutes([{ name: "Home", path: "" }]);
    
    const router = curi(history, routes, {
      route: [active(), ancestors()]
    });

    Route interactions are called via the router's route object.

    router.route.active("Home");
    // returns true when location.pathname = "/"
    
    router.route.pathname("Home");
    // returns "/"
  • sideEffects - An array of side effect objects.

    propertydescription
    effectAn observer that will be called whenever a response is generated.
    after(default false) controls whether the side effect is called before or after non-side effect observers.
    import scroll from "@curi/side-effect-scroll";
    
    const router = curi(history, routes, {
      sideEffects: [scroll()]
    });
  • external - Values that should be accessible to a route's resolve functions and response() function.

    Using external allows you to access APIs, data, etc. without having to be able to import it in the module where the routes are defined.

    const client = new ApolloClient();
    const router = curi(history, routes, {
      external: { client, greeting: "Hi!" }
    });
    const routes = prepareRoutes([
      {
        name: "User",
        path: "user/:id",
        resolve: {
          data(match, external) {
            // use the external object to make a query
            return external.client.query()
          }
        }
      }
    ]);
  • emitRedirects - When false (default is true), response objects with the redirectTo property will not be emitted to observers. This can be useful for avoiding an extra render, but should not be used on the server.

    const routes = prepareRoutes([
      {
        name: "Old",
        path: "old/:id",
        response({ params }) {
          // setup a redirect to the "New" route
          return {
            redirectTo: {
              name: "New",
              params
            }
          };
        }
      },
      {
        name: "New",
        path: "new/:id"
      }
    ]);
    
    const router = curi(history, routes, {
      emitRedirects: false                 
    });
    // navigating to "/old/2" will automatically redirect
    // to "/new/2" without emitting a response
  • automaticRedirects - When the initially matched route is synchronous and redirects, the router's automatic redirect will occur before any response handlers (registered with once() or observe()) are called. This means that they will be called with the response for the location that was redirected to instead of the initial location. This is fine on the client side, but causes issues with server side rendering. When automaticRedirects is false, the automatic redirect will not happen. Using automaticRedirects = false is recommend for server side rendering.

    const routes = prepareRoutes([
      {
        name: "Old",
        path: "old/:id",
        response({ params }) {
          // setup a redirect to the "New" route
          return {
            redirectTo: {
              name: "New",
              params
            }
          };
        }
      },
      {
        name: "New",
        path: "new/:id"
      }
    ]);
    const history = InMemory({ locations: ["old/1" ]});
    const router = curi(history, routes, {
      automaticRedirects: false                 
    });
    router.once(({ response }) => {
      // response = { name: "Old", ... }
    });
  • pathnameOptions - Curi uses path-to-regexp to handle route matching and pathname generation. path-to-regexp can take a custom encode function for creating pathnames, which you can specify with this options. You most likely will never need to use this.

    const router = curi(history, routes, {
      pathOptions: {
        encode: (value, token) => { /* ... */ }
      }
    });

Router Properties

The router has a number of properties for you to use when rendering your application.

once(fn, options)

The once() method takes a response handler function. If a response already exists, the function will be called immediately. Otherwise, the function will be called once a new response is created. The { initial: false } option can be used to prevent an immediate call even if a response already exists.

propertydescription
responseThe generated response object.
navigationThe navigation's action (PUSH, REPLACE, or POP) and the previous response object.
routerThe Curi router

When a matched route is async (it has resolve functions), a response will not be created until the async function(s) have resolved.

router.once(({ response }) => {
  // render the application based on the response
});
options
optiondefaultdescription
initialtrueWhen true, the function will be called immediately if a response exists. When false, the response function will not be called until the next response is emitted.
router.once(responseHandler, {
  initial: false
});
observe(fn, options)

The observe() method takes a response handler function. The response handler will be called every time a new response is emitted (and it a response already exists, the function will be called immediately). The { initial: false } option can be used to prevent an immediate call even if a response already exists.

propertydescription
responseThe generated response object.
navigationThe navigation's action (PUSH, REPLACE, or POP) and the previous response object.
routerThe Curi router

When a matched route is async (it has resolve functions), a response will not be created until the async function(s) have resolved.

router.observe(({ response }) => {
  // render the application based on the response
});
options
optiondefaultdescription
initialtrueWhen true, the function will be called immediately if a response exists. When false, the response function will not be called until the next response is emitted.
router.observe(responseHandler, {
  initial: false
});

observe() returns a function to stop calling the response handler function for new responses.

const stopObserving = router.observe(
  () => {...}
);
// the router will now call the observer for all responses

stopObserving();
// the router no longer calls the observer
cancel(fn)

With asynchronous routes, after a user begins navigation, but before the route's asynchronous actions have finished, the user does not have a good way to cancel the navigation. They can either refresh the page (causing a full reload) or click a link with the same URL as the current location, but neither of these are intuitive or ideal.

cancel() takes an observer function that will be called when navigation starts and when the navigation is finished. When the navigation starts, the observer function will be given a function to cancel the navigation. When the navigation finishes, the function will be called with undefined.

Calling cancel() returns a function to stop observing.

const stopCancelling = router.cancel(fn => {
  if (fn === undefined) {
    // the navigation has finished/been cancelled
  } else {
    // calling fn will cancel the navigation
  }
});
current()

The router.current() method returns the current response and navigation objects.

Note: If you call router.current() before the initial response has been emitted, the response and navigation properties will be null.
const router = curi(history, routes);
const tooSoon = router.current();
// tooSoon.response === null
// tooSoon.navigation === null

router.once(({ response, navigation }) => {
  const perfect = router.current();
  // perfect.response === response
  // perfect.navigation === navigation
});
route

The router's route interactions are accessed through the route property. These are used to interact with routes using their names.

pathname

Curi includes one built-in interaction, pathname, which generates location pathnames using the name of a route and an optional object containing any necessary params.

const routes = prepareRoutes([
  { name: 'User', path: 'user/:id' }
]);
const router = curi(history, routes);
const userPathname = router.route.pathname(
  'User',
  { id: '12345' }
);
// userPathname === '/user/12345'
refresh()

The refresh() function takes an array of new routes, which will replace the existing routes. The router will emit a new response based on the current location.

The function can be called without any arguments and it will emit a response using the existing routes.

const oldRoutes = prepareRoutes([...]);
const newRoutes = prepareRoutes([...]);

const router = curi(history, oldRoutes);
// generates responses using old routes

router.refresh(newRoutes);
// generates responses using new routes
history

The route's history object, in case you need to interact directly with that.

prepareRoutes

The prepareRoutes() export is used to build the routes for Curi. This will pre-compile paths for location matching and pathname building, which is particularly useful for server rendering.

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

const routes = prepareRoutes([
  { name: "Home", path: "" },
  // ...
  { name: "Not Found", path: "(.*)" }
]);
Warning: Passing a non-prepared routes array to curi() is still supported, but deprecated and will be removed in the next major version.

Route properties

route.name

A string, this must be unique for every route.

[
  { name: 'Home' },
  { name: 'Album' },
  { name: 'Not Found' }
];

route.path

A string pattern describing what the route matches. Whenever the router receives a new location, it will loop through the known route paths to determine which one matches the new location's pathname the best.

Curi usespath-to-regexp for paths, which enables routes to havepath parameters. When a route with parameters matches a location, the parameters will be be parsed from the location's pathname.

path strings should not have a leading slash.

Warning: path-to-regexp supports arrays and RegExps, but Curi only supports string paths. This is because Curi uses path-to-regexp to generate pathnames from a route's name, which it can only do from strings paths.
[
  { name: 'Home', path: '' },
  // when the pathname is a/yo, albumID = "yo"
  { name: 'Album', path: 'a/:albumID' },
  // the path (.*) matches every pathname
  { name: 'Not Found', path: '(.*)' }
];

// don't include a leading forward slash
// { name: 'Home', path: '/' }

route.resolve

The resolve object groups async functions that will be called when the route matches.

A route with any resolve functions is asynchronous, while one with no resolve functions is synchronous. You can read more about this is the sync or async guide.

resolve functions are called every time that a route matches the current location.

resolve functions will be passed an object with the matched route properties: name, params, partials, and location.

Note: You should not perform side effects (e.g. passing the loaded data to a Redux store) in resolve functions because it is possible that navigating to the route might be cancelled. If you must perform side effects for a route, you should do so in response().
const about = {
  name: 'About',
  path: 'about',
  resolve: {
    body: () => import('./components/About'),
    data: () => fetch('/api/about')
  }
};

route.response()

A function for modifying the response object. This returns an object whose properties will be merged with the matched route properties to create the "final" response.

Only valid properties will be merged onto the response; everything else will be ignored. The valid properties are:

  1. body - This is usually what you will render.

    import Home from "./components/Home";
    const routes = prepareRoutes([
      {
        name: "Home",
        path: "",
        response() {
          return { body: Home };
        }
      },
      // ...
    ]);
    // response = { body: Home, ... }
  2. status - A number. This is useful for redirects or locations caught by your catch-all route while using server-side rendering. The default status value is 200.

    {
      response(){
        return {
          status: 301,
          redirectTo: {...}
        };
      }
    }
    // response = { status: 301, ... }
  3. error - If an error occurs with the route's resolve methods, you might want to attach an error message to the response.

    {
      resolve: {
        test: () => Promise.reject("woops!")
      },
      response({ error }) {
        return { error };
      }
    }
    // response = { error: "woops!", ... }
  4. data - Anything you want it to be.

    {
      response() {
        return { data: Math.random() };
      }
    }
    // response = { data: 0.8651606708109429, ... }
  5. title - This can be used with @curi/side-effect-title to update the page's document.title.

    {
      response({ params }) {
        return { title: `User ${params.id}` };
      }
    }
    // when visting /user/2
    // response = { title: "User 2", ... }
  6. redirectTo - An object with the name of the route to redirect to, params (if required), and optional hash, query, and state properties.

    The other values are copied directly, but redirectTo will be turned into a location object using the object's name (and params if required).

    [
      {
        name: "Old Photo",
        path: "photo/:id",
        response({ params }) {
          return {
            redirectTo: { name: "Photo", params }
          };
        }
      },
      {
        name: "New Photo",
        path: "p/:id"
      }
    ]
    // when the user navigates to /photo/1:
    // response = { redirectTo: { pathname: "/p/1", ... } }

This function is passed an object with a number of properties that can be useful for modifying the response.

{
  response: ({ match, resolved }) => {
    // ...
  }
}
  • match

    An object with the matched route properties of a response.

    propertydescription
    namethe name of the matched route
    paramsroute parameters parsed from the location
    partialsthe names of any ancestor routes of the matched route
    locationthe location that was used to match the route
    keythe location's key, which is a unique identifier
  • resolved

    resolved is an object with the values resolved by the resolve functions.

    If a route isn't async, resolved will be null.

    // attach resolved data to the response
    const user = {
      name: 'User',
      path: ':id',
      resolve: {
        data: ({ params, location }) => (
          fetch(`/api/users/${params.id}`)
            .then(resp => JSON.parse(resp))
        ),
      },
      response: ({ resolved }) => {
        return {
          data: resolved.data
        };
      }
    }
  • error

    error is an error thrown by one of the route's resolve functions.

    // check if any of a route's resolve functions threw
    const user = {
      name: 'User',
      path: ':id',
      resolve: {
        data: ({ params, location }) => (
          fetch(`/api/users/${params.id}`)
            .then(resp => JSON.parse(resp))
        ),
      },
      response: ({ error, resolved }) => {
        if (error) {
          return { error };
        }
        return {
          data: resolved.data
        };
      }
    }

children

An optional array of route objects for creating nested routes. Any child routes will be matched relative to their parent route's path. This means that if a parent route's path string is 'one' and a child route's path string is 'two', the child will match when the pathname is 'one/two'.

// '/a/Coloring+Book/All+Night' will be matched
// by the "Song" route, with the params
// { album: 'Coloring+Book', title: 'All+Night' }
{
  name: 'Album',
  path: 'a/:album',
  children: [
    {
      name: 'Song',
      path: ':title'
    }
  ]
}

params

When path-to-regexp matches your paths, all parameters are extracted as strings. If you prefer for some route params to be other types, you can provide functions to transform params using the route.params object.

Properties of the route.params object are the names of params to be parsed. The paired value should be a function that takes a string (the value from the pathname) and returns a new value (transformed using the function you provide).

const routes = prepareRoutes([
  {
    name: 'Number',
    path: 'number/:num',
    params: {
      num: n => parseInt(n, 10)
    }
  }
]);

// when the user visits /number/1,
// response.params will be { num: 1 }
// instead of { num: "1" }

pathOptions

If you need to provide different path options than the defaults used by path-to-regexp, you can provide them with a pathOptions object.

Note: If a route has a children array property, it will always have the end path option set to false.

extra

If you have any additional properties that you want attached to a route, use the extra property. You will be able to use route.extra in any custom route interactions.

const routes = prepareRoutes([
  {
    name: 'A Route',
    path: 'a-route',
    extra: {
      transition: 'fade'
    }
  },
  {
    name: 'B Route',
    path: 'b-route',
    extra: {
      enter: 'slide-right'
    }
  }
]);