Features

Framework Integration

While Curi is a universal router, it still has to be integrated with whichever framework you are using to render your application. Currently, there are packages to easily use Curi with React and Vue applications (as well as one that provides basic Svelte support).

You can attach a body property to response object. Generally, this attached value should be a function or component (this would vary based on how your framework renders), but it can be anything you want it to be.

// routes.js
import User from './components/User';

const routes = [
  // ...
  {
    name: 'User',
    path: 'u/:userID',
    match: {
      response: ({ set }) => {
        set.body(User);
      }
    }
  }
];

Links are used for navigating between locations within an application. Links handle URI formatting for you, all you have to do is know the name (and parameters) of the route that you want to link to.

The @curi/react package provides a <Link> component for rendering links in React applications.

import Link from '@curi/react-link';

const NavLinks = () => (
  <div>
    <Link to='Home'>Home</Link>
    <Link to='User' params={{ userID: 4 }}>
      User Four
    </Link>
  </div>
);

With Vue, you register Curi using a Vue plugin from the @curi/vue package. That plugin will make the <curi-link> component available to use in your application.

<!-- NavLinks.html -->
<div>
  <curi-link to='Home'>Home</curi-link>
  <curi-link to='User' :params="{ userID: 4 }">
    User Four
  </curi-link>
</div>

Response Objects

Whenever the location changes (and on initial load), Curi will generate a response object with data based on the matching route. The properties of this object are what you can use to render your application. You can learn more about these in the rendering with responses guide.

The body property is the return value from the matching route's body function.

{
  key: '123',
  location: { pathname: '/u/456', ... },
  status: 200,
  name: 'User',
  body: function() { return ... },
  params: { userID: '456' },
  ...
}

data can contain values that you load using a route's match.every function. The response won't be be generated until after the match.every function has resolved, so if you use this property, you don't have to render a bunch of loading spinners or empty content while waiting for the data to be loaded.

{
  ...,
  data: {
    username: 'curi',
    id: '234235',
    color: '#222233'
  }
}

Expressive Route Matching with path-to-regexp

Curi uses path-to-regexp to define route paths. This allows you to define route parameters that will be parsed from the URI and added to the response object (when the route matches).

In the accompanying example code, when the User route matches, the response object's params object will have an id property whose value is parsed from the URI.

path-to-regexp offers a number of matching options, which you can learn more about from its documentation.

const routes = [
  {
    name: 'User',
    // when the User route matches, the "id"
    // value will be parsed from the pathname
    // and placed in the "params" property of
    // the response
    path: 'u/:id'
  }
];

Route Nesting

For nested routes, you only have to define the additional URI segments. Those will automatically be joined with any ancestor routes for you. If any ancestor routes have path parameters, those will be included in the response's params object.

const routes = [
  {
    name: 'Album',
    path: 'a/:albumID',
    match: {
      response: ({ set }) => {
        set.body(Album);
      }
    },
    children: [
      {
        name: 'Song',
        path: ':songID',
        match: {
          response: ({ set }) => {
            set.body(Song);
          }
        }
      }
    ]
  }
]

Given the above example routes, when a user visits the URI /a/4815/162342, we will get the following response object. The partials array contains the ancestor route "Song", which makes it easy to identify "active" ancestor routes.

// pathname = '/a/4815/162342'
{
  body: Song,
  params: { albumID: '4815', songID: '162342' },
  name: 'Song',
  partials: ['Album'],
  ...
}

Navigation Powered by hickory

Curi integrates with the hickory package to make navigation within your application very easy.

Choose between the browser, hash, and in-memory history types (depending on your environment).

import Browser from '@hickory/browser';
import Hash from '@hickory/hash';
import InMemory form '@hickory/in-memory';

Programmatically navigate using push, replace, and navigate (a combination of push and replace that replicates how anchors work).

const history = Browser();
history.push({ pathname: '/login' });
history.replace({ pathname: '/profile' });
history.navigate({ pathname: '/album/934' });

Of course, you never have to actually generate pathnames yourself. Curi's built-in pathname addon will generate pathnames given the name of a route (and any of that route's parameters). This addon is used by the various link components to generate anchor attributes.

const routes = [
  { name: 'Album', path: 'a/:albumID' }
];
const config = createConfig(history, routes);
const pathname = config.addons.pathname(
  'Album',
  { albumID: '3490' }
);
history.navigate({ pathname });

Code Splitting

Use the match.initial and match.response functions to add code splitting at your routes.

Note: This relies on a bundler like Webpack.

You can learn more about this with the code splitting guide.

const routes = [
  {
    name: 'User',
    path: 'users/:userID',
    match: {
      initial: () => (
        import('./components/User')
          .then(module => module.default)
      ),
      response: ({ resolved, set }) => {
        set.body(resolved.initial);
      }
    }
  }
  ...,
]

Server Side Rendering

Server side rendering is pretty much the same as client side rendering. The main difference is that you will use an in-memory history instead of a browser history.

import InMemory from '@hickory/in-memory';

function requestHandler(req, resp) {
  // create a history using the requested location
  const history = InMemory({
    locations: [req.url]
  });
  const config = createConfig(history, routes);

  config.respond((response, action) => {
    // render the markup. This will vary based on
    // your rendering library, but here we'll
    use React
    const markup = renderToString(
      <Navigator
        response={response}
        action={action}
        config={config}
        render={render}
      />
    );

    // insert the generated HTML into the full
    // HTML of the page and send the response
    res.send(fullPageHtml(markup));
  }, { once: true });
}