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 Routes (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 a resolve function that is called when the route matches the new 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 is no equivalent to onLeave with Curi. 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(match) { 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.

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 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). 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, 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, useCuri } from "@curi/react-dom";

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

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

ReactDOM.render((
  <Router>
    <App />
  </Router>
), holder);

/*
<Router>
  <App>
    <Message />
  </App>
</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.

The best way to get router data with Curi is to use the useCuri hook.

// React Router
export default withRouter(SomeComponent);

// Curi
function SomeComponent() {
  const { response } = useCuri();
  return ...
}

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.