redux-routable
v1.0.5
Published
Simple Routing Middleware for Redux
Downloads
73
Readme
Redux Routable
A simple Redux middleware for routing. Define the routes of your application, dispatch actions to navigate, and actions will be dispatched whenever the route changes.
Overview
- Use declarative functions to define your router.
- Dispatch navigation actions to change the location, open a route in a new tab, or go back or forward in history.
- Listen for the
ROUTE_CHANGEDaction in your reducers to persist state and execute side effects for the route, parameters, and hash.
Installation
npm install --save redux-routableAlong with installing this package, you'll also need to have
redux and
history installed.
Usage
Given that Redux Routable is a Redux middleware, you'll need to have the redux
package installed. You'll also need the history package, which provides
uniform API for navigating between locations across different environments. Read
the
"Intro"
section of their docs to decide how you'd like to create the history object
(using createBrowserHistory, createMemoryHistory, or createHashHistory).
Defining a Router
Let's start by defining a router:
import { Router, Route, Redirect, Fallback, Scope } from 'redux-routable'
const router = Router([
Route('home', '/'),
Scope('/user', Router([
Route('user', '/:id'),
Route('friends', '/:id/friends')
])),
Redirect('user', '/profile/:id'),
Fallback('notFound')
])From this configuration, we get an overview of what we can define in our router
with Redux Routable. We have named Routes that match on path patterns,
Redirects that take us from a path pattern to a defined route, Scopes that
allow us to nest Routers, and Fallbacks that let us match when nothing else
does. For more information, refer to the "Router Configuration
Constructors" section.
Creating the Middleware
Now let's create our middleware and create a Redux store with it:
import { applyMiddleware, createStore } from 'redux'
import { createMemoryHistory } from 'history'
import { createMiddleware } from 'redux-routable'
import reducer from './reducer'
const history = createMemoryHistory()
const middleware = createMiddleware(router, history)
const store = createStore(reducer, applyMiddleware(middleware))Initializing Your Application with the SYNC Action
Now that we have our Redux store, we can dispatch some actions. The first action
we'll want to dispatch is the SYNC action provided by Redux Routable, which we
can create with the sync action
creator:
import { sync } from 'redux-routable'
store.dispatch(sync())Dispatching the SYNC action will cause a ROUTE_CHANGED action to be
dispatched that corresponds to the current location:
{
type: ROUTE_CHANGED,
payload: { route: 'home', params: {}, hash: '' },
meta: { previous: undefined }
}Since we're using createMemoryHistory, the location defaults to /, and the
ROUTE_CHANGED action says that the route has been changed to home. You'll
want to dispatch the SYNC action at the start of your application, so that
reducers and other middleware can synchronize themselves to the current route,
parameters, and hash.
Dispatching Navigation Actions
If we want to programmatically navigate within our application, we can dispatch
any of the navigation actions (PUSH, REPLACE, OPEN, GO, GO_BACK,
GO_FORWARD). Just like the SYNC action, these actions can only be created
with their corresponding action creators. We'll use the PUSH action as an
example:
import { push } from 'redux-routable'
store.dispatch(push('user', { id: '123' }))This will do 2 things:
- The location will be changed to
/user/123. - A
ROUTE_CHANGEDaction will be dispatched:
{
type: ROUTE_CHANGED,
payload: { route: 'user', params: { id: '123' }, hash: '' },
meta: { previous: { route: 'home', params: {}, hash: '' } }
}All of the other navigation actions (with the exception of OPEN) will have the
same flow:
- Dispatch a navigation action.
- The location will be changed.
- A
ROUTE_CHANGEDaction will be dispatched.
The OPEN action is an exception to this flow, as dispatching this action will
cause a new window or tab to be opened instead of changing the location.
Handling ROUTE_CHANGED Actions
In order to use information from the location (route, params, and hash) in your
reducers or middleware, you need to listen to the ROUTE_CHANGED action that
gets dispatched by the middleware. If we want to keep track of the current
route, we can do so easily:
import { ROUTE_CHANGED } from 'redux-routable'
const currentRouteReducer = (state, { type, payload }) => {
if (type === ROUTE_CHANGED) {
return payload.route
} else {
return state
}
}What if we want a reducer that stores the ID of a user whenever we navigate to
the user route, and clears it whenever we navigate away? Here's how we can do
that:
import { ROUTE_CHANGED } from 'redux-routable'
const userIdReducer = (state, { type, payload }) => {
if (type === ROUTE_CHANGED) {
if (payload.route === 'user') {
return payload.params.id
} else {
return null
}
} else {
return state
}
}This use case is common enough, and reducers like this will likely be written repeatedly, so Redux Routable provides a helper for it:
import { paramsReducer } from 'redux-routable'
const userIdReducer = paramsReducer('user', ({ id }) => id)Redux Routable comes with some other helpers as well. The changedTo function
determines if an action is a ROUTE_CHANGED action for a specific route. This
can be useful to kick off side effects like loading data from an API whenever a
specific route or routes are navigated to. There are also the entered and
exited functions, which look at meta.previous to determine whether you're
going "from" or "to" a specific route or routes. These can be useful for page
transitions or loading/clearing data common to a set of routes.
Even though these helper functions cover some common use cases, you can handle
ROUTE_CHANGED actions however you'd like. Redux Routable does not prescribe
how you store your routing data or how you manage side effects.
Handling ROUTE_NOT_MATCHED and LOCATION_NOT_MATCHED Actions
Sometimes, things don't go exactly as planned, whether it's because of developer or user error. Redux Routable provides a couple of actions that allow you to handle errors related to routing.
The ROUTE_NOT_MATCHED action is dispatched whenever a navigation action is
dispatched but the route that it specifies cannot be matched to a route in
configuration. This is usually because a developer has passed a route to a
navigation action creator that doesn't exist in the router configuration. It
also might be because the params passed to a navigation action creator are
somehow invalid, as in a missing parameter or a parameter of the wrong type.
The LOCATION_NOT_MATCHED action is dispatched whenever the location is changed
but that location cannot be matched to a route in configuration. Since the user
is allowed to change the location to whatever they'd like, this action is likely
going to be dispatched, so it's a good idea to handle it. It's worth noting,
however, that if you have a Fallback without a path in your top-level
Router, then this action will never be dispatched, since that Fallback will
match any location. Whether you have a top-level Fallback should be determined
by whether you want to treat the user navigating to a "not found" route as an
error condition or a normal part of the application.
The shape of both of these actions is documented in the "Action Types" section.
API
All functions in this section are exported as named exports from the
redux-routable package.
Router Configuration Constructors
The router configuration constructors return objects that hold information about how to route in your application. Their purpose is to provide declarative configuration for the middleware.
Route(name, [path=''])A
Routewill match when the entire location matches the pattern given bypath.Redirect(to, [path=''])A
Redirectwill match when the entire location matches the pattern given bypath, but the middleware will redirect to the route referenced byto.Fallback(name, [path=''])A
Fallbackwill match when the beginning of the location matches the pattern given bypath. Sincepathdefaults to'', if no path is provided, it will always match.Scope(base, router)A
Scopeallows you to nest aRouterwithin anotherRouterby prepending a base path to thepathof each of the children of the innerRouter.Router(children)A
Routerrepresents the configuration needed by the middleware to enable routing with your application. When the location changes, the location will be matched against each child in order until a match is found. Any instance of the above router configuration functions are validRouterchildren.
The path parameter of Route, Redirect, and Fallback is matched with
path-to-regexp. Refer to their
documentation for path syntax.
Middleware
createMiddleware(router, history)This function takes a
Routeras its first parameter and ahistoryobject created using thehistorypackage. It returns a Redux middleware that can be passed to Redux'sapplyMiddleware()function.
Action Creators
Actions created by action creators in this section get "caught" by the middleware in order to execute navigation side effects. They do not have their types exposed as they are not meant to be handled by your reducers or other middleware.
sync()Dispatching this action is useful at the start of your application, as it will cause a
ROUTE_CHANGEDaction to be dispatched that corresponds to the current location, which will give your reducers and other middleware an opportunity to synchronize with the current route.
Navigation Action Creators
push(route, [params={}], [hash=''])Dispatching this action will change the location to match the specified
route, pushing a new entry onto the history stack.replace(route, [params={}], [hash=''])Dispatching this action will change the location to match the specified
route, replacing the current entry on the history stack.open(route, [params={}], [hash=''])Dispatching this action will open a new window or tab to the location specified by
route.go(offset)Dispatching this action will navigate to the location
offsetaway from the current location on the history stack.goBack()Dispatching this action will navigate to the previous location on the history stack.
goForward()Dispatching this action will navigate to the next location on the history stack.
Action Types
Actions with types in this section are created and dispatched by the middleware and are intended to be handled by your reducers or other middleware. They do not have action creators exposed as they are not meant to be manually constructed.
ROUTE_CHANGEDAn action with this type will be dispatched whenever the location is changed (and by dispatching a
SYNCaction) and the location is able to be matched with a route from the router configuration. Since all of the navigation actions (with the exception ofOPEN) change the location, you can expect this action to be dispatched immediately after dispatching any of those. Actions with typeROUTE_CHANGEDare what you should listen for in your reducers and other middleware, and they have this shape:{ type: ROUTE_CHANGED, payload: { route: ..., // Route name params: { ... }, // Path and query params hash: ... // Fragment identifier }, meta: { previous: ... // Payload of previous ROUTE_CHANGED action } }payload.paramsis populated by both path params (parsed withpath-to-regexp) and query params (parsed withquery-string).ROUTE_NOT_MATCHEDAn action with this type will be dispatched whenever a
PUSH,REPLACE, orOPENnavigation action is dispatched and the providedrouteorparamsis not able to be matched with a route in the router configuration. Themessageof theRouteMatchErrorin the payload will state the reason that the route could not be matched. These actions have this shape:{ type: ROUTE_NOT_MATCHED, error: true, payload: ..., // RouteMatchError meta: { route: ..., params: { ... }, hash: ... } }metawill have the same properties as thepayloadof theROUTE_CHANGEDaction above.LOCATION_NOT_MATCHEDAn action with this type will be dispatched whenever the location is changed (and by dispatching a
SYNCaction) and the location is not able to be matched with a route from the router configuration. This action will only end up being dispatched if you do not have aFallbackwithout apathin your top-levelRouter. These actions have this shape:{ type: LOCATION_NOT_MATCHED, error: true, payload: ..., // LocationMatchError meta: { location: { ... } } }meta.locationwill be the location that failed to match as provided by thehistorypackage. Read the "Properties" section of their README to see the properties on this object.
Helpers
match(route, matchable)This function will return whether a
routematches amatchable.paramsReducer(matchable, [awayVal=null], paramsSelector)This function creates a reducer that evaluates
paramsSelectoragainst thepayload.paramsof aROUTE_CHANGEDaction when navigated to the specifiedmatchable. When navigated away frommatchable, the reducer will evaluate toawayVal.changedTo(matchable)This function takes a "matchable" and returns a predicate that evaluates to
truewhen passed aROUTE_CHANGEDaction that matchesmatchableand evaluates tofalseotherwise.entered(matchable)This function takes a "matchable" and returns a predicate that evaluates to
truewhen passed aROUTE_CHANGEDaction wherematchablewas "entered" (the previousROUTE_CHANGEDaction does not matchmatchable, but the current one does) and evaluates tofalseotherwise.exited(matchable)This function takes a "matchable" and returns a predicate that evaluates to
truewhen passed aROUTE_CHANGEDaction wherematchablewas "exited" (the previousROUTE_CHANGEDaction matchesmatchable, but the current one does not) and evaluates tofalseotherwise.
The matchable parameter of all of the helper functions can be either a route
name, an array of route names, or a Redux Routable config object.
