redux-first-history

Redux First History - Make Redux 100% SINGLE-AND-ONLY source of truth!
Redux history binding for
Compatible with immer
- redux-immer
- redux-immutable
.
:tada: A smaller, faster, optionated, issue-free alternative to
connected-react-router
Main Goal
Use whatever you like. History will just work as it should.
useLocation() === state.router.location
this.props.history.location === state.router.location
this.props.location === state.router.location
withRouter.props.location === state.router.location
this.context.router.history.location === state.router.location
this.context.route.location === state.router.location
this.props.location === state.router.location
useLocation()[0] === state.router.location.pathname
You can mix redux, redux-saga, react-router, @reach/router & wouter
without any synchronization issue!
Why? Because there is no synchronization at all! There is only one history: reduxHistory!
- one way data-flow
- one unique source of truth
- no more location issue!
Demo : https://wy5qw1125l.codesandbox.io/ src: https://codesandbox.io/s/wy5qw1125l
Demo v6 : https://codesandbox.io/s/redux-first-history-demo-rr6-uccuw
Main features
- 100% one source of truth (store)
- No syncronization depending on rendering lifecicle (ConnectedRouter)
- No React dependency (we want history to be always in store!)
- 100% one-way data flow (only dispatch actions!)
- improve React shallowCompare as there is only one "location"
- support react-router v4 / v5 / v6
- support @reach/router 1.x
- support wouter 2.x
- support mix react-router, @reach/router & wouter in the same app!
- fast migration from existing project, with same
LOCATION_CHANGE
and push actions (taken from RRR) - handle Redux Travelling from devTools (that's a non sense in production, but at the end of the day this decision it's up to you ...)
Installation
Using npm:
$ npm install --save redux-first-history
Or yarn:
$ yarn add redux-first-history
Usage
store.js
import { createStore, combineReducers, applyMiddleware } from "redux";
import { composeWithDevTools } from "redux-devtools-extension";
import { createReduxHistoryContext, reachify } from "redux-first-history";
import { createWouterHook } from "redux-first-history/wouter";
import { createBrowserHistory } from 'history';
const { createReduxHistory, routerMiddleware, routerReducer } = createReduxHistoryContext({
history: createBrowserHistory(),
});
export const store = createStore(
combineReducers({
router: routerReducer
}),
composeWithDevTools(
applyMiddleware(routerMiddleware)
)
);
export const history = createReduxHistory(store);
export const reachHistory = reachify(history);
export const wouterUseLocation = createWouterHook(history);
store.js (with @reduxjs/toolkit)
import { combineReducers } from "redux";
import { configureStore } from "@reduxjs/toolkit";
import { createReduxHistoryContext, reachify } from "redux-first-history";
import { createBrowserHistory } from "history";
const {
createReduxHistory,
routerMiddleware,
routerReducer
} = createReduxHistoryContext({ history: createBrowserHistory() });
export const store = configureStore({
reducer: combineReducers({
router: routerReducer
}),
middleware: [routerMiddleware]
});
export const history = createReduxHistory(store);
app.js
import React, { Component } from "react";
import { Provider, connect } from "react-redux";
import { Router } from "react-router-dom";
import { store, history } from "./store";
const App = () => (
<Provider store={store}>
<Router history={history}>
//.....
</Router>
</Provider>
);
export default App;
app.js (react-router v6)
import React, { Component } from "react";
import { Provider, connect } from "react-redux";
import { HistoryRouter as Router } from "redux-first-history/rr6";
import { store, history } from "./store";
const App = () => (
<Provider store={store}>
<Router history={history}>
//.....
</Router>
</Provider>
);
export default App;
- just simple Router with no more ConnectedRouter!
- use
push
action creator from redux-first-history
if you need to dispatch location from saga
or connected components. - Probably, you already did it with
react-router-redux
or connected-react-router
(in this case you have only to replace the import!)
Abstract
While working with relatively large projects, it's quite common to use both redux
and react-router
.
So you may have components that take location from store, others that take location from router context, and others from withRouter HOC.
This sometimes could generate sync issue, due to the fact that many components are updated at different time.
In addition, React shallowCompare rendering optimization will not work as it should.
With redux-first-history
, you can mix components that get history from wherever,
they will always be tunneled to state.router.location !
Options
export const createReduxHistoryContext = ({
history,
routerReducerKey = 'router',
reduxTravelling = false,
selectRouterState = null,
savePreviousLocations = 0,
batch = null,
reachGlobalHistory = null
})
key | optional | description |
---|
history | no | The createBrowserHistory object - v4.x/v5.x |
routerReducerKey | yes | if you don't like router name for reducer. |
reduxTravelling | yes | if you want to play with redux-dev-tools :D. |
selectRouterState | yes | custom selector for router state. With redux-immutable selectRouterState = state => state.get("router") |
savePreviousLocations | yes | if > 0 add the key "previousLocation" to state.router, with the last N locations. [{location,action}, ...] |
batch | yes | a batch function for batching states updates with history updates. Prevent top-down updates on react : usage import { unstable_batchedUpdates } from 'react-dom'; batch = unstable_batchedUpdates |
reachGlobalHistory | yes | globalHistory object from @reach/router - support imperatively navigate of @reach/router - import { navigate } from '@reach/router' : usage import { globalHistory } from '@reach/router'; reachGlobalHistory = globalHistory |
Advanced config
- support "navigate" from @reach/router
import { createReduxHistoryContext, reachify } from "redux-first-history";
import { createBrowserHistory } from 'history';
import { globalHistory } from '@reach/router';
const { createReduxHistory, routerMiddleware, routerReducer } = createReduxHistoryContext({
history: createBrowserHistory(),
reachGlobalHistory: globalHistory,
});
- React batch updates: top-down batch updates for maximum performance. Fix also some update edge cases.
import { createReduxHistoryContext, reachify } from "redux-first-history";
import { createBrowserHistory } from 'history';
import { unstable_batchedUpdates } from 'react-dom';
const { createReduxHistory, routerMiddleware, routerReducer } = createReduxHistoryContext({
history: createBrowserHistory(),
batch: unstable_batchedUpdates,
});
Feedback
Let me know what do you think!
Enjoy it? Star this project! :D
credits & inspiration
- redux-first-routing
- react-router-redux
- connected-react-router
Contributors
See Contributors.
License
MIT License.