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:
responseis the new response objectnavigationis 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} />;
}Links
You will want to be able to navigate between routes in your application. React Router provides a Link component to do this, and so does Curi (through the @curi/react-dom package). There are a few differences to note between these two components:
React Router expects you to generate the pathname yourself, while Curi expects you to pass the name of the route that you want to navigate to. Any path parameters are passed to Curi’s
Linkusing theparamsprop.// React Router <Link to='/'>Home</Link> <Link to={`/inbox/${message}`}>Hello</Link> // Curi <Link name='Home'>Home</Link> <Link name='Message' params={{ message }}>Hello</Link>With React Router, any additional location properties are passed to the
Linkusing thetoobject. With Curi, these properties are passed using the prop name (hash,query&state).// React Router <Link to={{ pathname: '/inbox', hash: '#test' }}> Inbox </Link> // Curi <Link name='Inbox' hash='test'>Inbox</Link>Active detection with Curi is done using the
useActivehook. The hook takes the name of the route (and any required params) and returns a boolean to indicate if the route is active. You can also use itspartialoption to detect when ancestor routes are active (the opposite of React Router'sonlyActiveOnIndex).// React Router <Link to='/' onlyActiveOnIndex activeClassName='active' > Home </Link> // Curi // The useActive hook returns a boolean indicating // if a route is active const active = useActive("Home"); <Link name='Home' className={active ? "active" : ""}> Home </Link>
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.