@curi/router

About#

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

Installation#

If you have Node and NPM installed, you can install the package through npm (or yarn if you prefer).

npm install @curi/router
yarn add @curi/router

Prefer inline scripts? Every version is available through Unpkg. There are both full and minified versions available. You can access the package's exports through window.Curi.

<script src="https://unpkg.com/@curi/router@1.0.1/dist/curi-router.umd.js"></script>
<script type="text/javascript">
  window.Curi
</script>

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 Creating a Router 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 route objects for all valid routes in the application.

const routes = [
  { 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 = [{ 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()]
    });
  • 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 = [
      {
        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 observer()) 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 = [
      {
        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
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 = [
  { 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 = [...];
const newRoutes = [...];

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.

once#

once takes a function as its argument and returns a new function. The first time the returned function is called, it will call the function passed to once(). Every call after that will re-use the result from the first call.

The once() function is useful for any async route resolve functions that only need to be called once.

Note: This will not work for functions whose result depends on variables that will change for a route (i.e. loading data based on route params).
import { once } from "@curi/router";
            
const routes = [
  {
    name: "Menu",
    path: "menu",
    resolve: {
      // this function will be called every time the user
      // navigates to the "Menu" route
      nonCached: () => api.getItems(),
      // this function is only called the first time the
      // user navigates to the "Menu" route
      cached: once(() => api.getItems)
    }
  }
];

pathname#

Curi automatically includes a pathname route interaction for you to generate URL pathnames for routes. If you need to access this same ability outside of a router, you can import the pathname route interaction.

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

const pathnameGenerator = pathname();
// register routes
pathnameGenerator.register({ name: "Yo", path: "yo/:name" });
// generate pathname
const path = pathnameGenerator.get("Yo", { name: "joey" })
// path = "/yo/joey"

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 = [
      {
        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 = [
  {
    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 = [
  {
    name: 'A Route',
    path: 'a-route',
    extra: {
      transition: 'fade'
    }
  },
  {
    name: 'B Route',
    path: 'b-route',
    extra: {
      enter: 'slide-right'
    }
  }
];