React Router v2/3

Curi is mostly conceptually similar to React Router versions 2 and 3.

  1. Both use a centralized router.
  2. Both routers are made up of route objects (although with React Router some of these are disguised as JSX with <Route> components).
  3. With both Reaft Router and Curi, routes can be nested. This can be used to specify child routes that build off of the paths from their parent routes.

Migration from React Router v2/3 to Curi should not require a complete reworking of your application, but there are some key differences.

  1. Curi's routing is handled entirely outside of React; there are no <Route> components.
  2. With Curi, when a nested route matches, only that route renders. Any ancestor routes that also (partially) match are not rendered. This is different from React Router, where ancestors of the best matched route also render.

Routes

Let’s get started with setting up our routes.

With React Router

In React Router v2/3, there are two ways to define routes. You can either use JavaScript objects or JSX <Route>s (which React Router converts to JavaScript objects).

Both styles described above define the same route structure for three routes: /, /inbox, and /inbox/:message. Each one of these has a component that will be rendered when it matches. The /inbox/:message route has some methods defined to describe its behavior when the route enters, updates, and leaves.

// JavaScript objects
{
  path: '/',
  component: App,
  indexRoute: Home,
  childRoutes: [
    {
      path: 'inbox',
      component: Inbox,
      childRoutes: [
        {
          path: ':message',
          component: Message,
          onEnter: (next) => {...},
          onChange: (prev, next) => {...},
          onLeave: (prev) => {...}
        }
      ]
    }
  ]
// JSX
<Route path='/' component={App}>
  <IndexRoute component={Home} />
  <Route path='inbox' component={Inbox}>
    <Route
      path=':message'
      component={Message}
      onEnter={next => {...}}
      onChange={(prev, next) => {...}}
      onLeave={prev => {...}}
    />
  </Route>
</Route>

With Curi

Routes in Curi are always JavaScript objects. Like React Router, each route object has a path property that describes the path segments that the route matches. React Router v2/3 uses a custom path matcher, but Curi uses path-to-regexp. You can read learn how to format paths from the path-to-regexp repo.

First, we will define the names and paths for our routes.

Each route must also have a unique name. A route's name will be used for interacting with it. For example, to navigate to a route, you only have to know its name, not its URL.

The biggest difference between the Curi paths and the React Router paths is that with Curi, you never include a forward slash at the beginning of the path. This means that while the root path for React Router is '/', the root path for Curi is ''.

const routes = prepareRoutes([
  {
    name: 'Home',
    path: ''
  },
  {
    name: 'Inbox',
    path: 'inbox',
    children: [
      {
        name: 'Message',
        path: ':message'
      }
    ]
  }
]);

Next, we should add our components to each route. We will ignore the <App> component that is used in the React Router routes. That is not route specific and will be rendered by our application (assuming we actually need it).

With Curi, the router creates a "response" object when it matches locations. Some of the properties of the response are automatically set based on the location and the matching route. Others can be set by a route. This is done using the response() property, which is a function that returns an object whose properties will be added to the response. For this React application, we want a response's body property to be the React component associated with each route.

Note: Only known properties will be merged onto the response.
import { prepareRoutes } from "@curi/router";
            
import Home from './pages/Home';
import Inbox from './pages/Inbox';
import Mesage from './pages/Message';

const routes = prepareRoutes([
  {
    name: 'Home',
    path: '',
    response: () => {
      return {
        body: Home
      };
    }
  },
  {
    name: 'Inbox',
    path: 'inbox',
    response: () => {
      return {
        body: Inbox
      };
    },
    children: [
      {
        name: 'Message',
        path: ':message',
        response: () => {
          return {
            body: Message
          };
        }
      }
    ]
  }
]);

We are close to replicating our React Router routes, but we still need to implement the on___ methods for our :message route. With Curi, routes can have functions that are called when they match the new location. These are grouped under the route's resolve object. The resolve functions are called every time that a route matches a location.

With React Router, onEnter is called when the route first matches, while onChange is called when the same route matches a new location (e.g. with new path parameters). onEnter and onChange are nearly the same; the big difference between the two is that onChange will receive the previous props, which could be used to determine which props changed. The functionality for both onEnter and onChange can be covered using a resolve function.

There currently is no equivalent to onLeave with Curi. This is mostly because I haven’t seen a compelling need for it. It certainly could be implemented, but so far I have not found a reason to use that. If you have something you need this functionality for, please open up an issue in the GitHub repo.

The @curi/router route API documentation covers all of the route properties.

const routes = prepareRoutes([
  {
    name: 'Home',
    path: '',
    response: () => {
      return {
        body: Home
      };
    }
  },
  {
    name: 'Inbox',
    path: 'inbox',
    response: () => {
      return {
        body: Inbox
      };
    },
    children: [
      {
        name: 'Message',
        path: ':message',
        response: () => {
          return {
            body: Message
          };
        },
        resolve: {
          data: (route) => { return ... },
        }
      }
    ]
  }
]);

Once your routes have been defined, you can move on to creating your Curi router.

Creating the router

With React Router, you create your router by rendering a <Router>. That either takes the <Route> components as props or the route objects through its routes prop. The <Router> also takes a history prop, which is either one of the pre-routerured objects (browserHistory or hashHistory) or one that you create yourself.

import { Router, browserHistory } from 'react-router';
const routes = prepareRoutes([...]);
ReactDOM.render((
  <Router history={browserHistory} routes={routes} />
), holder);

With Curi, the router is created prior to rendering. It takes a Hickory history object, your routes array, and possibly an options object. Hickory is similar to the history package used by React Router, but has a slight modified API (easier navigation blocking and navigation that imitates how anchors work) and more convenient location objects (you can use a query object instead of having to manually create a search string).

import { curi, prepareRoutes } from '@curi/router';
import Browser from '@hickory/browser';
const history = Browser();
const routes = prepareRoutes([...]);
const router = curi(history, routes);

Rendering

We will walk through the rendering differences between React Router and Curi by looking at what happens in each when we navigate to the URI /inbox/test-message-please-ignore.

React Router v2/3

React Router uses the <Router> component to subscribe to location changes. Each time that the location changes, it walks over its routes and determines which route(s!) match.

React Router starts by rendering the root component. In the above router, that is the <App>. Next, our inbox route also matches, so React Router also renders our <Inbox> component. Finally, the URI /inbox/test-message-please-ignore also matches our :message route (which is concatenated with its parents to form the path /inbox/:message), so <Message> is rendered as well. Each child component is rendered by its parent, so we end up with a component tree that looks something like this:

With this structure, any routes with children will be rendered when one of the children matches. That means that those routes need to know how to render based on what type of match they have. For example, <Inbox> needs to know how to render for an exact match (the URI is /inbox) and for a partial match (/inbox/test-message-please-ignore). Also, if the <Inbox> needs to pass any props to <Message>, it has to use React.cloneElement, which works but is not the cleanest looking code.

<App>
  <Inbox>
    <Message>
  </Inbox>
</App>

Curi

With Curi, we also need to re-render our application every time that the location changes. We will do this by creating a root Curi component by calling the curiProvider() function, which comes from the @curi/react-dom package, and passing it our Curi router. While the name of this component is entirely up to you, we will refer to it as the <Router> here.

The <Router> will setup an observer on the provided router so that it can re-render your application whenever there is a new response. The <Router> expects a function as its children prop (a render-invoked function). This function renders the application using the response.

When the <Router>'s children() function is called, it will receive an object with three properties:

  1. response is the new response object
  2. navigation is an object with additional information about the navigation
  3. router is your Curi router (mostly useful if the function is defined in a separate file)

Above, we added response() functions to each route. The functions set React components as the body property of responses. We can now use response.body to render those components.

In the React Router section, we had three components that were rendered: <App>,<Inbox>, and <Message>. With Curi, only the most accurately matched route actually matches. That means that for the URL /inbox/test-message-please-ignore, the "Message" route will match, but its parent route, "Inbox" will not, so response.body will be the <Message> component. Unlike React Router, we don’t render <Inbox> because we did not match the inbox route.

import { curiProvider } from "@curi/react-dom";

const router = curi(history, routes);            
const Router = curiProvider(router);

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

/*
  <Router>
    <Message />
  </Router>
*/
Note: Wildcard routes ({ path: '(.*)' }) can be used to easily display a not found page for any location not matched by other routes.
const routes = prepareRoutes([
  // ...,
  {
    name: "Not Found",
    path: "(.*)",
    response() {
      return { body: NotFound };
    }
  }
]);

It was mentioned above that there is no need for the <App> component with Curi. If you want to have an <App> component, you can render it either inside of the children() function or as a parent of your <Router>. This can be useful for rendering content that is unrelated to specific routes, like a page header or menu.

Rendering the <App> inside of the children() function is necessary if any of the components rendered by the <App> are location aware components, since they need to access the Curi router (through React’s context, which the <Router> provides)

function render({ response }) {
  const { body:Body } = response;
  return (
    <App>
      <Body />
    </App>
  );
}
// or
function render({ response }) {
  const { body:Body } = response;
  return (
    <div>
      <Header />
      <Body />
      <Footer />
    </div>
  );
}

What about props that you want to send to your route components? Pass them to the <Body> component that you render. Props can be passed individually, but passing the whole response object is recommended.

function render({ response }) {
  const { body:Body } = response;
  return <Body response={response} />;
}

Accessing router props from nested components

React Router provides a withRouter higher-order component that will inject router props into the wrapped component.

Curi provides similar functionality with the Curious component.

Curious has a render-invoked children function, which you can use to inject the Curi router, the current response, and the current navigation object into components.

// React Router
export default withRouter(SomeComponent);

// Curi
export default () => (
  <Curious>
    {({ response }) => (
      <SomeComponent response={response} />
    )}
  </Curious>
);

At this point, hopefully you are comfortable with migrating from React Router v2/3 to Curi. If there are any concepts not covered here that you think should be, please feel free to open up an issue on GitHub.