React Router v2/3
Curi is mostly conceptually similar to React Router versions 2 and 3.
- Both use a centralized router.
- Both routers are made up of route objects (although with React Router some of these are disguised as JSX with
Route
components). - 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.
- Curi's routing is handled entirely outside of React; there are no
Route
components. - 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.
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 ''
.
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 respond
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.
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.
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.
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.
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.
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:
response
is the new response objectnavigation
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.
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)
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.
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
Link
using theparams
prop.With React Router, any additional location properties are passed to the
Link
using theto
object. With Curi, these properties are passed using the prop name (hash
,query
&state
).Active detection with Curi is done using the
useActive
hook. 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 itspartial
option to detect when ancestor routes are active (the opposite of React Router'sonlyActiveOnIndex
).
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 useResponse
hook.
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.