React Router v4

React Router v4 isn't like most other routers because it lacks a centralized configuration. Migrating to Curi mostly involves re-centralizing your routes to simplify route management.

Routes

Let’s get started with setting up our routes.

With React Router

With React Router v4, <Route>s are defined in components. They are usually grouped together under a <Switch> so that only a single route from a group renders. Nested routes are rendered inside of the compnent rendered by the parent <Route>

import { Route, Switch } from "react-router-dom";
            
const App = () => (
  <Switch>
    <Route exact path="/" component={Home} />
    <Route path="/inbox" component={Inbox} />
  </Switch>                 
);

// the <Inbox> matches nested routes (and includes
// a <Route> for "exact" /inbox matches)
const Inbox = ({ match }) => (
  <Switch>
    <Route
      exact
      path={match.path}
      component={Messages}
    />
    <Route
      exact
      path={`${match.path}/:message`}
      component={Message}
    />
  </Switch>
);

With Curi

Routes in Curi are JavaScript objects. They are grouped together in an array of "top-level" routes. Nested routes are grouped under their parent route's children property.

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

Each route must 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.

Curi routes can have a response() property, which is a function that returns an object of properties to merge onto the response that we will be using to render. For this React application, we want a response's body property to be the React component associated with each route.

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
          };
        }
      }
    ]
  }
]);

With React Router v4, a component's lifecycle methods are used for loading data, code splitting, and other non-rendering tasks. 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.

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

const routes = prepareRoutes([
  {
    path: '',
    response: () => {
      return {
        body: Home
      };
    }
  },
  {
    path: 'inbox',
    response: () => {
      return {
        body: Inbox
      };
    },
    children: [
      {
        path: ':message',
        response: () => {
          return {
            body: Message
          };
        },
        resolve: {
          body: (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> component. This may be a <BrowserRouter>, a <HashRouter>, a <MemoryRouter>, or a plain <Router> that you pass your own history instance to. The <___Router> components create a history instance for you using props passed to the component.

import { BrowserRouter } from 'react-router-dom';

ReactDOM.render((
  <BrowserRouter>
    <App />
  </BrowserRouter>
), 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 URL with the pathname /inbox/test-message-please-ignore.

React Router

React Router matches routes while it renders. It uses the <Router> component to listen for location changes. Each time that the location changes, the application re-renders.

The <Switch> will iterate over its children <Route>s. The first route, "/" has an exact prop, so it only matches when the pathname is "/". Since it is not, the next <Route> will be checked. The next route, "/inbox" matches the beginning of the pathname "/inbox/test-message-please-ignore". It is not an exact match, but that route does not do exact matching, so React Router will render its component, <Inbox>.

The <Inbox> has its own <Switch> to iterate over. Its first route only matches "/inbox" exactly, so it moves on to the next route, which has a message route param. This route matches and stores "test-message-please-ignore" as match.params.message. The <Message> component will then be rendered, which has access to the message param.

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

const App = () => (
  <Switch>
    <Route exact path="/" component={Home} />
    <Route path="/inbox" component={Inbox} />
  </Switch>                 
);

const Inbox = ({ match }) => (
  <Switch>
    <Route
      exact
      path={match.path}
      component={Messages}
    />
    <Route
      exact
      path={`${match.path}/:message`}
      component={Message}
    />
  </Switch>
);

/*
<BrowserRouter>
  <App>
    <Inbox>
      <Message>
    </Inbox>
  </App>
</BrowserRouter>
*/

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