react-redux-saga-router
Connecting your url and redux state

Elegant powerful routing based on the simplicity of storing url as state
To install:
$ npm i -S react-redux-saga-router
Table of Contents
Simple example
Let's expand upon the todo list example from the redux documentation
In the sample application, we can create new todos, mark them as finished, and filter
the list to display all of them, just active todos, and just completed todos. We can
add URL routing quite simply by focusing on the filtering state.
We'll respond to these 3 URLs:
/filter/SHOW_ALL
/filter/SHOW_ACTIVE
/filter/SHOW_COMPLETED
To do this, we'll need to add four items to the app:
- The router reducer, for storing routing state.
- A route definition, mapping url to state, and state to url
- The route definition within the app itself
- include redux-saga and react-redux, and pass in the sagaMiddleware and connect
reducers/index.js:
import { combineReducers } from 'redux'
import routing from 'react-redux-saga-router/reducer'
import todos from './todos'
import visibilityFilter from './visibilityFilter'
const todoApp = combineReducers({
todos,
visibilityFilter,
routing
})
export default todoApp
Routes.js:
import React from 'react'
import Routes from 'react-redux-saga-router/Routes'
import Route from 'react-redux-saga-router/Route'
import * as actions from './actions'
const paramsFromState = state => ({ visibilityFilter: state.visibilityFilter })
const stateFromParams = params => ({
visibilityFilter: params.visibilityFilter || 'SHOW_ACTIVE'
})
const updateState = {
visibilityFilter: filter => actions.setVisibilityFilter(filter)
}
export default () => (
<Routes>
<Route name="filters" path="/filter/:visibilityFilter"
paramsFromState={paramsFromState}
stateFromParams={stateFromParams}
updateState={updateState}
/>
</Routes>
)
index.js:
import React from 'react'
import { render } from 'react-dom'
import { Provider, connect } from 'react-redux'
import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
import router from 'react-redux-saga-router'
import todoApp from './reducers'
import App from './components/App'
let store = createStore(todoApp, undefined, applyMiddleware(sagaMiddleware))
router(sagaMiddleware, connect)
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
then add these lines inside App.js:
import Routes from './Routes'
const App = () => (
<div>
<Routes />
<AddTodo />
<VisibleTodoList />
<Footer />
</div>
)
Internal linking with <Link>
Note that if we want to set up a menu of urls, react-redux-saga-router provides a
<Link>
component that should be used for all internal links. It uses the to
prop in place of href. An onClick handler may be passed to handle the click in
a custom fashion. All other props will be passed through to the internal <a>
tag.
If you wish to replace the current url instead of pushing, use the replace
prop
instead of the to
prop.
Unlike any other router, the <Link>
component can also create abstract routes
from a list of route parameters. With this route declaration:
const routes = () => (
<Routes>
<Route name="biff" path="/this/:fancy(/:thing)" />
</Routes>
)
we can create a link like so:
const App = () => (
<div>
<Link route="biff" fancy="hi" thing={some.dynamicValue} />
</div>
)
and if the dynamic value refers to 123
the route will link to /this/hi/123
Extending the example: asynchronous state loading
What if we are loading the todo list from a database? There will be a short delay while
the list is loaded, and the user will just see an empty list of todos. If they add a todo,
the todo id could suddenly conflict with todos the user creates, which would erase them on the
database load. Better is to display a different component while loading.
To implement this with our router, you will use:
- a loading component that will be displayed when the todos are loading
- a "Toggle" higher order component that is used to switch on/off display of a
component or its loading component
- an asynchronous program to load the todos from the database.
- an additional way of marking whether state is loaded or not in the store, and
actions and reducer code to capture this state.
redux-saga is an excellent solution for expressing complex asynchronous actions in a
simple way. Although react-redux-router-saga uses redux-saga internally and highly
recommends it, you can write your asynchronous loader in any manner you choose, whether
it is a thunk or an epic or fill-in-your-favorite.
For this example, we will assume that you can add a simple "loaded" field to the todos
reducer, and actions to set it to true or false.
Let's design the loading component first:
Loading.js:
import React from 'react'
export default () => (
<div>
<h1>Loading...</h1>
</div>
)
Asynchronous loading of the todo items from the database can be accomplished with a very
simple saga. The saga assumes that the todos can be accessed via a REST service that
returns JSON, and uses the axios library to make an xhr call to retrieve it from the
server at the "/getTodos"
address.
loadTodosSaga.js:
import { call, put } from 'redux-saga/effects'
import axios from 'axios'
import * as actions from './actions'
export default function *loadTodos() {
yield put(actions.setLoaded(false))
const todos = yield call([axios, axios.get], '/getTodos')
yield put(actions.setTodos(todos))
yield put(actions.setLoaded(true))
}
Now let's create a Toggle. A Toggle is a higher order component that responds to state in order
to turn on or off the display of a component, like a toggle switch. It takes 2 callbacks as parameters.
Each callback receives the state as a parameter and should return truthy or falsey values. The first is
used to determine whether the main component should be displayed. The second optional callback is used
to determine whether state is still loading, and if so, whether to display the loading component.
By default, if no loading callback is passed in, a Toggle assumes that the state is
loaded.
In our example, there is only 1 route, and so we will display it if our state is marked
as loaded. If not, we will not display the component. Instead, we will display the
loading component. Here is the source:
TodosToggle.js:
import Toggle from 'react-redux-saga-router/Toggle'
export default Toggle(state => state.loaded, state => !state.loaded)
The TodosToggle is a component that accepts 2 props: component
and loadingComponent
.
component
should be a React component or connected container to display if the
Toggle condition is satisfied, and loadingComponent
should be a React component or connected
container to display if the loading condition is satisfied.
Note that if both callbacks return true, then the loading component will be displayed.
Finally, the usage of TodosToggle is straightforward.
in App.js:
import React from 'react'
import Footer from './Footer'
import AddTodo from '../containers/AddTodo'
import VisibleTodoList from '../containers/VisibleTodoList'
import Routes from './Routes'
import Loading from './Loading'
import TodosToggle from './TodosToggle'
const App = () => (
<div>
<Routes />
<AddTodo />
<TodosToggle component={VisibleTodoList} loadingComponent={Loading} />
<Footer />
</div>
)
export default App
Now our component will display the todo list only when it has loaded.
Common use case: displaying a component when a route is selected
In most applications, there are menus that select components based on the user
selecting a sub-application. To display components whose sole display criteria is
the selection of a route, use a RouteToggle
import RouteToggle from 'react-redux-saga-router'
const TodosRoute = RouteToggle('todos')
In this way, you can display several components scattered around a layout template
that are route-specific without having to make a new layout template just for that route,
or doing any strange contortions.
A RouteToggle
accepts all the arguments of Toggle afterwards:
import RouteToggle from 'react-redux-saga-router'
const TodosRoute = RouteToggle('todos', state => state.whatever === 'hi')
The example above will only toggle if the todos route is active and the whatever
portion of state is equal to 'hi'
A RouteToggle
can be thought of
as a simpler version of this source code:
import Toggle from 'react-redux-saga-router/Toggle'
import { matchedRoutes } from 'react-redux-saga-router/selectors'
const TodosRoute = Toggle(state => matchedRoutes(state, 'todos'))
Available selectors for Toggles
The following selectors are available for use with Toggles. import as follows:
import * as selectors from 'react-redux-saga-router/selectors'
matchedRoute(state, name)
import * as selectors from 'react-redux-saga-router/selectors'
import Toggle from 'react-redux-saga-router/Toggle'
export Toggle(state => selectors.matchedRoute(state, 'routename'))
matchedRoute
accepts a single route name, or an array of route names to match.
By default, it matches on any route. To enable strict matching (all routes must match)
pass in true to the third parameter of matchedRoute
import * as selectors from 'react-redux-saga-router/selectors'
import Toggle from 'react-redux-saga-router/Toggle'
export Toggle(state => selectors.matchedRoute(state, ['route', 'subroute'], true))
This is useful for strict matching of a sub-route path.
Note that a convenience Toggle, RouteToggle
exists to match a route:
import RouteToggle from 'react-redux-saga-router/RouteToggle'
export RouteToggle('routename', state => otherconditions())
This selector returns true if the route specified by 'routename'
is active
noMatches
import * as selectors from 'react-redux-saga-router/selectors'
import Toggle from 'react-redux-saga-router/Toggle'
export Toggle(state => selectors.noMatches(state))
This selector returns true if no routes match, and can be used for an error component
or default component
stateExists
import * as selectors from 'react-redux-saga-router/selectors'
import Toggle from 'react-redux-saga-router/Toggle'
export Toggle(state => state.whatever, state => selectors.stateExists(state, ))
This toggle is designed to be used to detect whether state has loaded. Pass in
a skeleton of the state shape and it will traverse the state to determine whether it exists.
Here is a sample from an actual project:
import Toggle from 'react-redux-saga-router/Toggle'
import * as selectors from 'react-redux-saga-router/selectors'
export const check = state => selectors.stateExists(state, {
campers: {
ids: []
},
groups: {
ids: [],
groups: {},
selectedGroup: (group, state) => {
if (!group) return true
if (state.groups.ids.indexOf(group) === -1) return false
const g = state.groups.groups[group]
if (!g) return false
if (g.type && !state.ensembleTypes.ensembleTypes[g.type]) return false
if (g.members.length) {
if (g.members.some(m => m ? !state.campers.campers[m] : false)) return false
}
return true
}
},
ensembleTypes: {
ids: [],
},
})
export default Toggle(state => state.groups.selectedGroup, check)
The selector verifies that the campers and ensembleTypes state areas have an ids
member that is an array, and that the groups state area has ids and groups set up.
For selectedGroup, a callback is called, passed the value of the state item plus the
entire state tree. The callback verifies that the selected group's state is internally
consistent and when everything is set up, returns true.
What about complex routes like react-router <Route>
?
For a complex application, there will be components that should only display on certain
routes. For example, an example from the react-router documentation:
render((
<Router history={browserHistory}>
<Route path="/" component={App}>
<Route path="about" component={About}/>
<Route path="users" component={Users}>
<Route path="/user/:userId" component={User}/>
</Route>
<Route path="*" component={NoMatch}/>
</Route>
</Router>
), document.getElementById('root'))
There are 3 things happening here.
- The App structure is dictated by the declaration of routes.
- The
App
component will have as its children
prop set to About
or Users
or
NoMatch
, depending on the url. - In addition, if the route is
/user/123
the App
component
will have its children set to Users
with its children set to User
This complexity is forced by the design of react-router. How can we express these routes
using react-redux-saga-router?
We need 2 things:
- Toggles for routes and for selected user, and for no match
- Plugging in the Toggles where they should be displayed within the React tree.
import * as selectors from 'react-redux-saga-router/selectors'
import Toggle from 'react-redux-saga-router/Toggle'
import RouteToggle from 'react-redux-saga-router/RouteToggle'
const AboutToggle = RouteToggle('about')
const UsersToggle = RouteToggle(['users', 'user'])
const SelectedUserToggle = Toggle(state => !!state.users.selectedUser,
state => usersLoaded(state) && state.users.user[state.users.selectedUser])
const NoMatchToggle = Toggle(state => selectors.noMatches(state))
Now, to plug them in:
App.js
render() {
return (
<div>
<AboutToggle component={About} />
<UsersToggle component={UsersRoute} />
<NoMatchToggle component={NoMatch} />
</div>
)
}
Routes.js:
import React from 'react'
import Routes from 'react-redux-saga-router/Routes'
import Route from 'react-redux-saga-router/Route'
import * as actions from './actions'
const paramsFromState = state => ({ userId: state.users.selectedUser || undefined })
const stateFromParams = params => ({ userId: params.userId || false })
const updateState = {
userId: id => actions.setSelectedUser(id)
}
const exitParams = {
userId: undefined
}
export default () => (
<Routes>
<Route name="about" path="/about" />
<Route name="users" path="/users" />
<Route name="user" path="/user/:userId"
paramsFromState={paramsFromState}
stateFromParams={stateFromParams}
exitParams={exitParams}
updateState={updateState}
/>
</Routes>
)
Note that the else
prop of a Toggle higher order component can be used to display an
alternative component if the state test is not satisfied, but the component state is loaded.
So in our example, we want to display the user list if a user is not selected, so we set our
else
to Users
and our component
to User
UsersRoute.js:
render() {
return (
<div>
<SelectedUserToggle component={User} else={Users}/>
</div>
)
}
Easy!
Dynamic Routes
Sometimes, it is necessary to implement dynamic routes that are calculated from a parent
route. This can be done quite easily.
const Parent = () => (
<Routes>
<Route name="parent" path="/parent/path" />
</Routes>
)
const Child = ({ parentroute }) => (
<Routes>
<Route name="child" parent={parentroute} path=":hi" />
</Routes>
)
In this case, the child will make its path match /parent/path/:hi
enter
/exit
hooks
To implement enter or exit hooks you can listen for the ENTER_ROUTES
or
EXIT_ROUTES
action to perform actions such as loading asynchronous state.
Here is a sample implementation:
import * as types from 'react-redux-saga-router/types'
function *enter() {
while (true) {
const action = yield take(types.ENTER_ROUTE)
if (action.payload.indexOf('myroute') === -1) continue
do {
const second = yield take(types.EXIT_ROUTE)
} while (second.payload.indexOf('myroute') === 1)
}
}
Anything can be done in this code, including forcing a route to change, like a traditional
enter/exit hook. Because it is so trivial to implement this with the above code, the
event loop that listens for URL changes and state changes does not listen for enter/exit
hooks directly.
updating state on route exit
All routes that accept parameters and map them to state will need to unset that state
upon exiting the route. react-redux-saga-router can do this automatically for any
route with only optional parameters, such as:
/path(/:optional(/:second_optional))
However, for routes that have a required parameter such as:
/path/:required
you need to tell the router how to handle this case. If the required parameter should
be simply set to undefined upon exiting, then you need to explicitly pass this
into the exitParams
prop for <Route>
exitParams = {
required: undefined
}
const Routes = () => (
<Routes>
<Route
name="test"
path="/path/:required"
stateToParams={...}
paramsToState={...}
updateState={...}
exitParams={exitParams}
/>
</Routes>
)
If you wish to dynamically set up the parameters based on existing parameters, pass
in a function that accepts the previous url's params as an argument and returns the
exit params:
exitParams = params => ({
required: params.required,
optional: undefined
})
const Routes = () => (
<Routes>
<Route
name="test"
path="/path/:required(/:optional)"
stateToParams={...}
paramsToState={...}
updateState={...}
exitParams={exitParams}
/>
</Routes>
)
Code splitting and asynchronous loading of Routes
Routes can be loaded at any time. If you load a new component asynchronously (using
require.ensure, for instance), and dynamically add a new <Routes><Route>...
inside that
component, the router will seamlessly start using the route. Code splitting has never
been simpler.
Server-side Rendering
When rendering routes on the server, there are 2 options. No changes need be made
to the component source. However, because of the way server rendering works, multiple
actions and re-renders will occur when setting up routes. To avoid the performance
penalty for complex applications, an optional third parameter to the router setup
can be used to pass in the routes. The definition of routes is an object with the
same keys as the props one would pass to a <Route>
tag.
so instead of:
router(sagaMiddleware, connect)
exitParams = params => ({
required: params.required,
optional: undefined
})
const Routes = () => (
<Routes>
<Route
name="test"
path="/path/:required(/:optional)"
stateToParams={...}
paramsToState={...}
updateState={...}
exitParams={exitParams}
/>
</Routes>
)
one would use:
exitParams = params => ({
required: params.required,
optional: undefined
})
router(sagaMiddleware, connect, {
name: 'test',
path: '/path/:required(/:optional)',
stateToParams=...,
paramsToState=...,
updateState={...},
exitParams={exitParams},
})
The same setup can be used on both client and server for root routes, so there is no
need to keep the <Routes>
and <Route>
elements in your component tree if
you choose to initialize on start-up. You should continue to use the comopnents for
dynamic routes loaded later.
Explicitly changing URL
A number of actions are provided to change the browser state directly, most useful
for menus and other direct links.
react-redux-saga-router uses the history package
internally. The actions mirror the push/replace/go/goBack/goForward methods as
documented for the history package.
Reverse routing: creating URLs from parameters
react-redux-saga-router uses the route-parser
package internally, which allows us to take advantage of some great features.
The makePath
function is available for creating a url from params, allowing
separation of the URL structure from the data that is used to populate it.
import { makePath } from 'react-redux-saga-router'
const a = <Route name="foo" path="/my/:fancy/path(/:wow/*supercomplicated(/:thing))" />
console.log(makePath('foo', {
fancy: 'pants'
}))
console.log(makePath('foo', {
fancy: 'pants',
wow: 'oops'
}))
console.log(makePath('foo', {
fancy: 'pants',
wow: 'yes',
supercomplicated: '/this/works/just/fine'
}))
console.log(makePath('foo', {
fancy: 'pants',
wow: 'yes',
supercomplicated: '/this/works/just/fine',
thing: 'wheeee'
}))
Why a new router?
react-router is a mature router for
React that has a huge following and community support. Why create a new router?
In my work with react-router, I found that it was not possible to achieve
some basic goals using react-router. I couldn't figure out a way to store state from
url parameters and easily change the url from the state when using redux. It is the
classic two-way binding issue: if there are 2 sources of state, they will fight and
cause unexpected bugs.
In addition, I moved to redux for state because the tree of components in React rarely
corresponds to the way data is used. In many cases, I find myself rendering different
portions of the component tree using the same data. So I will have 2 React components
in totally different parts of the component tree using the same piece of data.
With react-router, I found myself duplicating a lot of content with a single component,
or using complex routing rules to enable displaying this information.
With react-redux-saga-router, multiple components can respond to a route change anywhere
in the React component tree, allowing for more modular design. It is important to note
here that react-router version 4 addresses this design flaw by using a similar design
principle to react-redux-saga-router. Great minds think alike. However, by coupling
tightly the route definition and the component display, you are still limited to using
a single component per route, and so the ability to display different components for
the same route would require using some clever hacks such as coupling both components into
a single component that chooses which one to render based on the route params passed in
from react-router. The below solution is more performant both because the components
are not rendered at all if the route is not satisfied.
import React from 'react'
import Routes from 'react-redux-saga-router/Routes'
import Route from 'react-redux-saga-router/Route'
import Toggle from 'react-redux-saga-router/Toggle'
import { connect } from 'react-redux'
import * as actions from './actions'
const albumRouteMapping = {
stateFromParams: params => ({ id: params.album, track: +params.track }),
paramsFromState: state => ({
album: state.albums.selectedAlbum.id,
track: state.albums.selectedTrack ? state.albums.selectedTrack : undefined
}),
updateState: {
id: id => actions.selectAlbum(id),
track: track => actions.playTrack(track)
}
}
const TrackToggle = Toggle(state => state.albums.selectedTrack,
state => state.albums.selectedTrack
&& state.albums.allTracks[state.albums.selectedTrack].loading)
const AlbumToggle = Toggle(state => state.albums.selectedAlbum)
const AlbumList = ({ albums, selectAlbum }) => (
<ul>
{albums.map(album => <li key={album.id} onClick={selectAlbum(album.id)}>{album.name}</li>)}
</ul>
)
const AlbumDetail = ({ album, playTrack }) => (
<ul>
<li>Album details</li>
<li>...(stuff from the {album.name}</li>
{album.tracks.map(track => <li key={track.id} onClick={playTrack(track.id)}>{track.name}</li>)}
</ul>
)
const AlbumSummary = ({ album }) => {
<h1>
{album.name}
</h1>
}
const TrackPlayer = ({ track }) => {
<div>
<h1>{track.title}</h1>
<AudioPlayer audio={track.audio} />
</div>
}
const AlbumSummaryContainer = connect(state => ({ album: state.albums.selectedAlbum }))(AlbumSummary)
const AlbumListContainer = connect(state => ({ albums: state.albums.allAlbums }),
dispatch => ({ selectAlbum: id => dispatch(actions.selectAlbum(id)) }))(AlbumList)
const AlbumDetailContainer = connect(state => ({ album: state.albums.selectedAlbum }),
dispatch => ({ playTrack: id => dispatch(actions.selectTrack(id)) }))(AlbumDetail)
const TrackPlayerContainer = connect(state => ({ track: state.albums.tracks[state.albums.selectedTrack] }))(TrackPlayer)
const MyComponent = () => (
<div>
<Routes>
<Route name="main" path="/" />
<Route name="albumlist" path="/albums(/album/:album(/track/:track))"
{...albumRouteMapping}
/>
</Routes>
<TwoPanelLayout>
<Panel>
<AlbumToggle component={AlbumSummaryContainer} />
<TrackToggle component={TrackPlayerContainer} />
</Panel>
<Panel>
<AlbumToggle component={AlbumDetailContainer} else={AlbumListContainer} />
</Panel>
</TwoPanelLayout>
</div>
)
In addition, declaring new routes in asynchronously loaded code is trivial with this
design. One need only put in <Routes>
declarations in the child code and the new routes
will be added.
Principles
Most routers start from an assumption that the url determines what part of the application
to display. This first results in a tree of urls mapping to components. Because routes
are defined by the URL, it then becomes necessary to provide hooks and an index route, and
an unknown route and so on and so forth.
However clever one is, this results in a very subtle logic flaw when using redux. Redux-based
applications consider the store state to be a single source of truth. As such, general
state is not stored inside components, or pulled out of the client-side database or the
url state from pushState/popState. Read more about the state debate.
URL state is just another asynchronous input to redux state
We are trained to think of the browser URL as some kind of magic all-knowing state container.
Simply because it is there and the user can directly change it to any value. But how different
is this really than a database accessed on the server via asynchronous xhr? Or even
synchronous localStorage? Let's stop thinking of the URL as a state container. It
is an input that we can use to create state.
When the URL changes, it should cause a state change in the redux store
We want our URL to change the way the application works. This allows users to bookmark a
particular view, such as an email (/inbox/message/243) or a particular todo list filter
(/todos/all or /todos/search/house)
When the state changes in the redux store, it should be reflected in the URL
If a user clicks on something that affects the application state by triggering an action,
such as selecting an email to view, we want the URL to then update so the user can bookmark
that application state or share it.
This single principle is the reason for the existence of this router.
Route definition is separate from the components
Because URL state is just another input to the redux state, we only need to define
how to transform URLs into redux state. Components then choose whether to render based
on that state. This is a crucial difference from every other router out there.
Components are explicitly used where they go, and can be moved anywhere
With traditional routers, you must render the component where the route is declared.
This creates rigidity. In addition, with programs based on react-router, the link
between where a component exists and the route lives in the router. The only indication
that something "routey" is happening is the presence of {this.props.children}
which
can make debugging and technical debt higher. This router restores the natural tree and
layout of a React app: you use the component where it will actually be rendered in the
tree. Less technical debt, less confusion.
The drawback is that direct connection between URL and component is less obvious. The
tradeoff seems worth it, as the URL is just another input to the program. Currently,
the relationship between database definition and component is just as opaque, and that
works just fine, this is no different.
IndexRoute, Redirect and ErrorRoute are not necessary
Use Toggle and smart (connected) components to do all of this logic. For example, an
error route is basically a toggle that only displays when other routes are not selected.
You can use the noMatches
selector for this purpose. An indexRoute can be implemented
with the matchedRoute('/')
selector (and by defining a route for '/').
A redirect can be implemented simply by listening for a URL in a saga and pushing a new
one:
import { replace } from 'react-redux-saga-router'
import { ROUTE } from 'react-redux-saga-router/types'
import { take, put } from 'redux-saga/effects'
import { createPath } from 'history'
import RouteParser from 'route-parser'
function *redirect() {
while (true) {
const action = yield (take(ROUTE))
const parser = new RouteParser('/old/path/:is/:this')
const newparser = new RouteParser('/new/:is/:this')
const params = parser.match(createPath(action))
if (params) {
yield put(replace(newparser.reverse(params)))
}
}
}
Easy testing
Everything is properly isolated, and testable. You can easily unit test your route
stateFromParams and paramsFromState and updateState properties. Components are
simply components, no magic.
To set up routes for testing in a unit test, the synchronousMakeRoutes
functions is
available. Pass in an array of routes, and use the return in the reducer
import { synchronousMakeRoutes, routerReducer } from 'react-redux-saga-router'
describe('some component that uses routes', () => {
let fakeState
beforeEach(() => {
const action = synchronousMakeRoutes([
{
name: 'route1',
path: '/route1'
},
{
name: 'route1',
path: '/route2/:thing',
stateToParams: state => state,
paramsToState: params => params,
update: {
thing: thing => ({ type: 'changething', payload: thing })
}
}
])
fakeState = {
routing: routerReducer(undefined, action)
}
})
it('test something', () => {
})
})
You will need to set this up for any <Link>
components that use route to generate
the path, and any components that contain <Router>
or <Route>
tags when rendering
them.
License
MIT License
Thanks

Huge thanks to BrowserStack for providing
cross-browser testing on real devices, both automatic testing and manual testing.