Features

Easy React Integration

Curi is not just a React router, but React is currently the best supported rendering library for Curi.

Use the body property of routes to specify each route's component:

const Home = () => <div>Home</div>;
const User = props => <div>User {props.params.userID}</div>;
const NotFound = () => <div>Page not found...</div>;

const routes = [
  { name: 'Home', path: '', body: () => Home },
  { name: 'User', path: 'u/:userID', body: () => User },
  { name: 'Not Found', path: '*', body: () => NotFound }
];

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

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

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

The <Navigator> is responsible for re-rendering the application every time the location changes, using the render function.

import Navigator from '@curi/react-navigator';

ReactDOM.render((
  <Navigator config={config} render={(response) => {
    const { body:Body } = response;
    return (
      <div>
        <Nav />
        <Body />
      </div>
    );
  }}
));

There are a number of other Curi + React components, but <Navigator> and <Link> are the only ones that you'll need to be familiar with while getting started.. You can see the others via the @curi/react page.

Information Rich Response Objects

Whenever the location changes (and on initial load), Curi will generate a response object with data 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.

There isn't one "right" way to render using the response object, but it is useful for body to be a function that will render the content for the route. (The body property of a response is the value returned by calling the body property of a route.)

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

The data property of the response can contain data that you load using a route's load function. The response won't be be generated until after the data has fully loaded, 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'
  }
}

Powerful 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'
  }
];

No Hassle Nested Routes

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',
    body: () => Album,
    children: [
      {
        name: 'Song',
        path: ':songID',
        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';

Navigate to new locations using push, replace, and navigate (a combination of push and replace that duplicates 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. Instead, you should use Curi's built-in pathname addon to create them for you.

The <Link>s from @curi/react-link and @curi/vue use the pathname addon internally to generate the href attribute of the anchor elements that they render.

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

Simple Code Splitting

Use the preload and body properties 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 store = {};

const routes = [
  {
    name: 'User',
    path: 'users/:userID',
    preload: () => import('./components/User')
      .then(module => { store['User'] = module.default; }),
    body: () => store['User']
  }
  ...,
]

Straightforward 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.

Use config.ready to wait for the response object to be created, then render using the response object that that resolves with.

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.ready().then(response => {
    // render the markup. This will vary based on your
    // rendering library, but here we'll use React
    const markup = renderToString(
      <Navigator
        config={config}
        response={response}
        render={render}
      />
    );

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