Creating a Router

The Router#

The router is the controller of the single-page application. A router is created using a history object and a routes array.

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

const history = Browser();
const routes = [...];
const router = curi(history, routes);

History#

The history object manages navigation between locations within an application.

There are three types of history to choose from; which one you use depends on where your application is running.

  1. The browser history is used for applications running in a browser.

    If you use the browser history, your application should be hosted on a server that can handle dynamic requests. This either means a server with real time route matching (like an Express server) or through configuration where a fallback page is served when the request doesn't map to a real file on the server.

    You can use @curi/static to generate static HTML pages for a browser history powered application.

    import Browser from "@hickory/browser";
    const browserHistory = Browser();
  2. The hash history is a fallback history for applications running in a browser.

    A hash history should only be used if you cannot configure the server to respond to requests that don't match files on the server. Most static file hosts are configurable, so you probably don't need to use this.

    import Hash from "@hickory/hash";
    const hashHistory = Hash();
  3. The in-memory history is used for applications not running in a browser. For example, the in-memory history is used on the server, in a React Native app, and during testing.

    import InMemory from "@hickory/in-memory";
    const inMemoryHistory = InMemory();

If you are not familiar with how single-page applications interact with a server, this article should help: Single-Page Applications and the Server.

Locations#

The history object will map URLs into location objects. Only the pathname, query (search), and hash segments are used; locations ignore the domain and protocol segments of a URL.

When matching loactions to routes, only the pathname is used.

// https://www.example.com/page?key=value#trending
location = {
  pathname: "/page",
  query: "key=value"
  hash: "trending"
}

The query value of a location is a string by default, but the history object can be configured to automatically parse it into an object.

You can choose whichever query parsing/stringifying package you prefer. Some of the most popular are qs, query-string, and querystring.

import { parse, stringify } from "qs";
import Browser from "@hickory/browser";

const history = Browser({
  query: { parse, stringify }
});
  
// https://www.example.com/page?key=value#trending
location = {
  pathname: "/page",
  query: { key: "value" }
  hash: "trending"
}

For more details on the history objects and their APIs, please check out the Hickory documentation.

Routes#

The routes array defines the valid locations within an application. When the router receives a new location, it matches it against the routes to generate a response.

Each route is an object that has a unique name and a path string, which defines what locations the route matches.

Note: path strings do not start with a slash.

Routes can be nested. A child route's path will build on the paths from any ancestor routes.

You will almost always want to include a "catch all" route to match any "invalid" locations and render a 404 page. The path "(.*)" matches every location. During development, Curi will log a warning in the console to let you know if you forgot to include a catch all route.

const routes = [
  {
    name: "Home",
    path: ""
  },
  {
    name: "Album",
    path: "photos/:albumID",
    children: [
      {
        name: "Photo",
        // matches /photos/6789/12345 with params
        // { albumID: "6789", photoID: "12345" }
        path: ":photoID"
      }
    ]
  },
  {
    name: "Not Found",
    path: "(.*)"
  }
];

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(
  "Photo",
  {
    albumID: "abcd",
    photoId: "98765"
  }
);
// pathname = "/photos/abcd/98765"

Response Handlers#

When the router has created a response, it emits it to any response handlers. There are three types of response handlers: observers, one time functions, and side effects.

Side effects are passed to the router when you are creating it. You can read more about them in the side effects guide.

One time functions are passed to the router using router.once(). These are response handlers that will only be called one time. If a response already exists, then the response handler will be called immediately (unless configured not to). Otherwise, the one time response handler will be called after the next response is emitted.

Observers are passed to the router using router.observe(). Unlike one time functions, these will be called for every response emitted by the router (until you tell the router to stop calling it). You most likely will not need to call this yourself. The different framework implementations (@curi/react-dom, @curi/react-native, @curi/vue, and @curi/svelte) setup observers for you.

const router = curi(history, routes);

const stop = router.observe(({ response }) => {
  console.log('new response!', response);
});
// ...
stop();
// no longer observing

If you have any asynchronous routes (routes with resolve functions), router.once() should be used to delay the initial render until after the initial response is ready.

// wait for initial response to be ready
router.once(() => {
  // safe to render async routes now
});

Rendering#

Rendering is left to whatever rendering library you are using. The way that Curi interfaces with each of them varies, but they all use observers to be notified when there is a new response.

@curi/react-dom uses a <Router> with a render-invoked children function that will be called whenever there is a new response.

In React applications, response.body should be a React component, so rendering the application means creating an element from response.body.

The React Basics Tutorial gets into more detail about how this works.

// React
const Router = curiProvider(router);

ReactDOM.render((
  <Router>
    {({ response }) => {
      const { body:Body } = response;
      return <Body />;
    }}
  </Router>
), document.getElementById('root'));

@curi/vue sets up reactive objects that update when there is a new response. <component :is> can be used to render the body component.

The Vue Basics Tutorial details how to use Vue and Curi.

// Vue
Vue.use(CuriPlugin, { router });
new Vue({
  el: '#app',
  template: '<app />',
  components: { app }
});

@curi/svelte uses the Svelte store and <svelte:component> to render.

// Svelte
const store = curiStore(router);
new app({ target, store });