Changes in version 1.x
State Shape
redux-json-router
now uses the redux-first-routing
package internally, following its state shape:
{
router: {
pathname: '/nested/path/',
search: '?with=query',
queries: {
with: 'query'
},
hash: '#and-hash'
},
...
}
Link
The optional replace
prop was removed. Instead, you can now specify the desired navigation action via the action
prop:
<Link to="/about" />
<Link to="/about" action="replace" />
<Link action="goBack" />
<Link action="goForward" />
Redux JSON Router
redux-json-router
is a minimal router intended for use with client-rendered React/Redux applications.
Features
- Declarative routing — The routing configuration is defined declaratively with JSON, or with plain JavaScript.
- Redux-first routing — The URL is just a regular part of Redux state, and can be updated by dispatching actions.
- Practical handling of browser history — The URL held in the browser history and Redux store stay in sync behind the scenes, so that the UI can always trust the URL in the store as its source of truth.
- Code-splitting — Code-splitting and asynchronous loading of routes is easily enabled with a webpack loader.
- React bindings —
<Router/>
for matching/loading/rendering routes, and <Link/>
for push/replace navigation.
Quick Links
Motivation
Background
I know what you're thinking - yet another React/Redux router? Really?
Yes, there are many existing solutions out there already. Of course, you already know react-router
. But you may have also heard of solutions like redux-little-router
or universal-router
.
Every router has its strengths - and, as with any library, choosing a router comes down to finding the one that best suits your needs. Here are some of the categories that similar routers target:
- Environment: Browser-only, or universal
- Libraries: React-only, Redux (React optional), or other
- Routing config: JSX, plain JS, or JSON-based
redux-json-router
similarly targets a subset of the above categories. It attempts to answer the question: What is the minimal API needed in a Redux-first router for client-rendered React/Redux applications with a JSON-based routing configuration?
Redux-First Routing
Key point: Like with any other complex/dynamic state, the application view should depend solely on the URL held in the Redux store (a single source of truth).
In modern browsers, the URL state and history are held in window.location
and window.history
. We can manipulate the browser history directly using the history API, but even better, we can utilize the awesome history
module to accomplish this.
Without Redux-first routing, a React/Redux application view may depend on URL state from outside of the store, like this:
history → React Router ↘
View
Redux ↗
history → React Router ↘
↕ View
Redux ↗
With Redux-first routing, a React/Redux application view depends solely on the URL from the Redux store.
history
↕
Redux → View
redux-json-router
accomplishes Redux-first routing by making the URL a regular part of the state tree, allowing Redux actions to dispatch URL changes, and keeping the store and browser history in sync behind the scenes.
Here's what the URL looks like on the state tree:
{
...,
router: {
url: '/redux/json/router?is=cool#yup',
hash: 'yup',
queries: {
is: 'cool'
},
paths: [
'redux',
'json',
'router'
],
previous: {
url: '/previous/route',
hash: '',
queries: {},
paths: [
'previous',
'route'
]
}
}
}
Installation
npm install --save redux-json-router
API
redux-json-router
has a reasonably small API. Here's a look at the exports in src/index.js
:
export { createBrowserHistory } from './history/createBrowserHistory';
export { startListener } from './history/startListener';
export { routerMiddleware } from './redux/middleware';
export { routerReducer } from './redux/reducer';
export { push, replace, go, goBack, goForward, manualChange } from './redux/actions';
export { PUSH, REPLACE, GO, GO_BACK, GO_FORWARD, MANUAL_CHANGE } from './redux/constants';
export { RouterContainer as Router } from './react/Router';
export { LinkContainer as Link } from './react/Link';
- History API
- Redux API
routerMiddleware(history)
- intercepts router actions (
push
, replace
, go
, goBack
and goForward
) to update the browser history, before continuing/breaking the middleware chain
routerReducer
- parses and adds the URL to the Redux state
routerActions
- used to dispatch URL updates; middleware intercepts and calls equivalent
history
navigation actions
- push(href) — updates the current and previous URL in the Redux state
- replace(href) — updates the current URL, but not the previous URL, in the Redux state
- go(index) — intercepted by
routerMiddleware
; startListener
subsequently dispatches manualChange
action - goBack() — intercepted by
routerMiddleware
; startListener
subsequently dispatches manualChange
action - goForward() — intercepted by
routerMiddleware
; startListener
subsequently dispatches manualChange
action - manualChange(href) — updates the current and previous URL in the Redux state
routerConstants
- public action types for use in user-defined middleware
- React components
<Router/>
- matches the current URL with the routing configuration, loads, then renders the page
- props:
<Link/>
- used for internal navigation (as opposed to
<a/>
, for external navigation) - props:
- to (string) — required; the internal URL (pathname + query + hash) to navigate to (eg.
'/about'
, '/blog?posted=2017'
, '/blog/post/1#introduction'
) - replace (boolean) — optional; set to true for the link to dispatch a
replace
action (default: push
) - onClick (function) — optional; specify a callback to run on click, before the push/replace action is dispatched
- Webpack loader (optional)
route-loader
- translates a
.json
routing configuration file into JavaScript; see Routing Configuration for acceptable JSON, and Webpack Configuration for set-up instructions - options:
- debug (boolean) — optional; if
true
, logs the output to the console (default: false
) - chunks (boolean) — optional; if
true
, splits routes without a specified chunk
property into separate chunks; if false
, adds pages without a specified chunk
into the main code chunk (default: true
)
Usage
Let's look at how we'd add redux-json-router
to a React/Redux application. We'll only make a few changes:
- Routing config — We'll define the routes in a
routes.json
or routes.js
file. - Redux config — We'll add the Redux reducer and middleware to the
store.js
configuration file. - App entry point — We'll add a bit of boilerplate to
index.js
and render the app with <Router/>
. - Webpack config (optional) — To load
routes.json
, we'll add a custom loader to webpack.config.js
.
Routing Configuration
Declare your routes in a routes.json
file with an array of "route objects":
[
{
"path": "/",
"page": "./pages/Home",
"chunk": "main"
},
{
"path": "/docs",
"page": "./pages/Docs",
"chunk": "main",
"children": [
{
"path": "/:id",
"page": "./pages/Post"
}
]
},
{
"path": "*",
"page": "./pages/Error"
}
]
Route objects are defined as follows:
type RouteObject {
path: string,
page: string,
chunk?: string,
children?: [RouteObject]
}
The bundled webpack loader is used to translate routes.json
into JavaScript that can be read by the <Router/>
component. Alternatively, you may choose to write the routing config in JavaScript yourself:
export default [
{
path: '/',
load: () => Promise.resolve(require('./pages/Home').default),
},
{
path: '/docs',
load: () => Promise.resolve(require('./pages/Docs').default),
children: [
{
path: '/:id',
load: () => new Promise((resolve, reject) => {
try {
require.ensure(['./pages/Post'], (require) => {
resolve(require('./pages/Post').default);
});
} catch (err) {
reject(err);
}
}),
},
],
},
{
path: '*',
load: () => new Promise((resolve, reject) => {
try {
require.ensure(['./pages/Error'], (require) => {
resolve(require('./pages/Error').default);
});
} catch (err) {
reject(err);
}
}),
},
];
Redux Configuration
In your Redux config, add routerReducer
to the root reducer under the router
key, and add routerMiddleware
to your Redux middlewares, passing it the history singleton created in the application entry point (as shown in the following section).
import { combineReducers, applyMiddleware, compose, createStore } from 'redux';
import { routerReducer, routerMiddleware } from 'redux-json-router';
import { otherReducers, otherMiddlewares } from './other';
const makeRootReducer = () => combineReducers({
...otherReducers,
router: routerReducer
});
function configureStore(history, initialState = {}) {
const middlewares = [...otherMiddlewares, routerMiddleware(history)];
const enhancers = [applyMiddleware(...middlewares)];
return createStore(makeRootReducer(), initialState, composeEnhancers(...enhancers));
}
Application Entry Point
In your app's entry point, import the routing config, create the history singleton, create the store with the history singleton, and call startListener
to initialize the router state in the store and start listening for external actions. Finally, render the application using <Router/>
inside the Redux <Provider/>
.
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import { createBrowserHistory, startListener, Router } from 'redux-json-router';
import configureStore from './store';
import routes from './routes.json';
const history = createBrowserHistory();
const store = configureStore(history);
startListener(history, store);
render(
<Provider store={store}>
<Router routes={routes} />
</Provider>,
document.getElementById('app'));
Webpack Configuration (Optional)
To use the included webpack loader, import route-loader
directly into your webpack config:
const routes = [path.resolve(__dirname, './app/routes.json')];
const config = {
...,
module: {
rules: [
...,
{
test: /\.json$/,
exclude: routes,
loader: 'json-loader',
},
{
test: /\.json$/,
include: routes,
loader: 'redux-json-router/lib/route-loader',
options: {
},
},
...
Credits
This project was heavily inspired by similar work and research on JavaScript/React/Redux routing including:
Contributing
Contributions are welcome and are greatly appreciated!
Feel free to file an issue, start a discussion, or send a pull request.
License
MIT