react-redux-with-path
v0.0.6
Published
A version of react-redux designed for cases that want to access sub-tree of the state. and take it as state in mapStateToProps and mapDispatchToProps
Downloads
9,264
Readme
react-redux-with-path
Problems
When you start to build a large react app (we call it main app) which may consist of multiple smaller apps (we call them component apps), you will possibly get stuck with some problems such as:
- The integration of the main app and its component apps should be as little as possible. Ideally, we can develop them separately and the component apps are pluggable.
- Component apps shouldn't be aware of the existence of each other and the main app either. For example, the states of other apps and the main app should be invisible to any component app.
Specifically, you may encounter the following problems in development of the main app:
Every component app should keep its own store state in terms of redux. You would prefer to place it in a sub-tree of the main app store state just like a namespace for it.
When using utilities like mapStateToProps, mapDispatchToProps, Provider, connect from react-redux, you have to do something like:
const mapStateToProps = state => ( { property: state.app.reducer.property } );
But what you really want is:
const mapStateToProps = state => ( { property: state.reducer.property } );
which means the component app take its own state as the global store state.
When the main app have several identical component apps inside. There can be several identical reducers listening to a single action. If it is not intended, you have to find out a way to ask each reducer to listen to the action that is really dispatched to it.
If you want your main app to load component apps conditionally, you probably want to load the view, reducer, redux-saga and do initialisation on demand, instead of required all component apps at the beginning.
If you are facing similar problems above, you probably want to see the following discussion and take a look at this package react-redux-with-path.
Solution
namespace
As discussed above, we need to create namespace for each app. Inspired by How to dynamically load reducers for code splitting in a Redux application. We're able to inject reducers on demand and use combineReducers to create a namespace for each component app and the main app. We can do
function createReducer(reducersOnDemand) {
return combineReducers({
mainApp: mainAppReducer,
...reducersOnDemand,
});
}
function injectReducersOnDemand(store, reducerOnDemand) {
store.reducersOnDemand[path] = reducerOnDemand;
store.replaceReducer(createReducer(store.reducersOnDemand));
}
Along this way, the hierarchies of both combined reducer and store state become
.
├── componentApp1
│ ├── reducer1
│ ├── reducer2
│ └── reducer3
├── componentApp2
├── componentApp3
└── mainApp
We define the first level directory names (componentApp1, componentApp2...) as the path of each app. The path can be the component app name (which won't work in the case that the main app can have several identical component apps) or an unique app id for each component app instance.
Component App Isolation
As mentioned above, developing each component app as the whole app is a nice-to-have feature. Especially when we use Provider and connect utilities from react-redux. In this package react-redux-with-path, we provide two APIs SubProvider and connect to enable the normal way of developing each component app. See APIs part for details.
APIs
The two apis should be used together.
SubProvider [will be renamed to PathProvider]
As we know, the Provider from react-redux enable you to propagate the dispatch utility and state object from the redux store to any sub-component that is connected to, namely the sub-component wrapped with connect, connect(mapStateToProps, mapDispatchToProps)(SubComponent)
. In the same way, our SubProvider provides enable you to propagate the path to sub-components. More precisely, the path is provided to our connect API.
import { SubProvider } from 'react-redux-with-path';
const componentAppJSX = (
<SubProvider path={path}>
<ComponentApp />
</SubProvider>
);
connect
After wrapped with SubProvider, our connect api will provide any sub-component of component app with its own state, which is the sub-tree the main app store state. The normal usage:
const mapDispatchToProps = {
actionA: actionCreatorA,
actionB: actionCreatorB,
} // don't bindActionCreators here, let the connect api do it for you
const mapStateToProps = state => ({
propA: state.reducer.propA,
propB: state.reducer.propB,
});
export default connect(mapStateToProps, mapDispatchToProps)(SubComponent);
Boom! the component app get its own state and it is not aware of other part of the state tree.
Install
npm install react-redux-with-path --save
Notes
Now, this package can only help you namespace each component app, and it works in the case that component apps are not identical. You may worry about the collision of actions with same types from different component apps. But that can be avoided if you follow the ducks-modular-redux. However, there exists problems in more complicated situations. For example, the problem 3, 4 in our Problems section. Inspired by redux issues #1628, our solution to problem 3 and 4 is to wrap the actions of each component app with an additional field called path, and the reducers of each component app with a path check. The former is actually done in the connect api. For the latter, you have to do it yourself like
function wrapReducerWithPathCheck(reducer, path) {
return (state = {}, action) => {
if (!isActionFromRightPath(action, path) && !isActionFromRedux(action)) {
return state;
}
let hasChanged = false;
let previousState;
if (Object.keys(state).length > 0) {
previousState = state;
}
const nextState = reducer(previousState, action);
hasChanged = hasChanged || nextState !== previousState;
return hasChanged ? nextState : state;
};
}
This wrapReducerWithPathCheck utility is not shipped with the current version of react-redux-with-path. We may add that when it is ready for production or not add that if we find out a more elegant approach.