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, Routes 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 respond 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: '',
    respond: () => {
      return {
        body: Home
      };
    }
  },
  {
    name: 'Inbox',
    path: 'inbox',
    respond: () => {
      return {
        body: Inbox
      };
    },
    children: [
      {
        name: 'Message',
        path: ':message',
        respond: () => {
          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 a resolve function that is called when thee routes matches the new location.

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

const routes = prepareRoutes([
  {
    path: '',
    respond: () => {
      return {
        body: Home
      };
    }
  },
  {
    path: 'inbox',
    respond: () => {
      return {
        body: Inbox
      };
    },
    children: [
      {
        path: ':message',
        respond: () => {
          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 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 function, your routes array, and possibly an options object. Hickory is similar to the history package used by React Router, but has an API tailored for asynchronous applications.

import { curi, prepareRoutes } from '@curi/router';
import { browser } from '@hickory/browser';
const routes = prepareRoutes([...]);
const router = createRouter(browser, 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.

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 Routes. 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". 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 createRouterComponent 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 uses a context provider that makes a response available to other components in the application using the useResponse hook.

The useResponse hook returns an object with two properties:

  1. response is the new response object
  2. navigation is an object with additional information about the navigation

The router can also be accessed throughout the application using the useRouter hook.

Above, we added respond 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 { createRouterComponent, useResponse } from "@curi/react-dom";

const router = createRouter(browser, routes);
const Router = createRouterComponent(router);

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

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

/*
<Router>
  <App>
    <Message />
  </App>
</Router>
*/
const routes = prepareRoutes([
  // ...,
  {
    name: "Not Found",
    path: "(.*)",
    respond() {
      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 (
    <React.Fragment>
      <Header />
      <Body />
      <Footer />
    </React.Fragment>
  );
}

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 ResponseConsumer component.

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

// React Router
export default withRouter(SomeComponent);

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

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.

  1. Routes
    1. With React Router
    2. With Curi
  2. Creating the router
  3. Rendering
    1. React Router
    2. Curi
  4. Links
  5. Accessing router props from nested components