Notice No implementation exist yet, see Readme Driven Development
The problem:
- Bad cohesion - in a typical redux application it's difficult to understand how it works since it consists of many small (good !) related functions spread out in many different files and folders.
- Bad isolation - many functions (redux's actions and react-redux's connect) uses the global redux state object. Only reducers works on subtrees of the state tree.
- Too many level of indirections - React props referencing redux state via mapStateToProps functions, reducers references actions via strings, async action creators references redux state.
The solution:
- Makes your (async) action creator only see a branch of the redux state tree.
- Wraps your redux actions and reducers in one object which we call context.
- Let the
redux-context
library nest your context and setup nested reducers. - Make the folder structure mirror your redux state
- Keep shared actions and state in parent folders, share it via React props to child components.
- Use the
react-redux-context
lib's connect method (we don't map from the root react state object like in thereact-redux
connect method).
npm install --save redux-context
One solution to improve cohesion is to organize your source code by feature instead of by type.
An example of a feature based folder structure:
src/index.js
src/context.js
src/feature_a/index.js
src/feature_a/component1.js
src/feature_a/component2.js
src/feature_a/context.js
src/feature_b/featureb1/index.js
src/feature_b/featureb1/component.js
src/feature_b/featureb1/context.js
src/feature_b/
Another solution to improve cohesion is to keep actions and reducers in the same file.
Let say we want to develop a simple application showing a list of news headlines. If a headline is clicked then the news body will be shown for that headline. In the example below we only have one feature (see below how we split this into two features and folders: news-list and news-body).
The redux-context
requires that you create context objects (plain Javascript objects) with two properties: actions
and reducers
. This object will later be used by the redux-context
library to setup reducers and context aware getState methods.
src/news/context.js
// an Async action creator, works in a simimlar way to the thunk middleware
function getBody(id) {
// all actions declared by the context can be injected like this for async action creators
return (dispatch, getState, {getBodyPending, getBodySuccess, getBodyError}) => {
// only do this request if not already loaded itch
if (!getState().newsBody[id]) { // Notice, getState does not return the global state !!
getBodyPending(id); // Notice, no need to dispatch since it's already bound
api.then(getBodySuccess, getBodyError)]}
}
}
}
export default {
actions: {
getBodyFailure: (err) => ({type: 'getBodyFailure', err}),
getBodySuccess: (id, response) => ({type: 'getBodySuccess', id, response}),
getBodyPending: (id) => ({type: 'getBodyPending', id}),
getBody
}
reducers: {
newsBody: (state={}, action) => action.type === 'getBodySuccess' ? action.data : state,
newsList: (state, action) => {},
}
}
We want the state object described above to be mounted at the root of the redux state object with key news
.
Action creator and the connect method in the news feature should only see the {newsBody: {}, newsList: []}
state object.
The structure of the redux state object:
{
news: {
newsBody: {}
newsList: []
}
}
This structure is created with another context, the root context.
src/context.js
import newsContext from './news/context'
export default {
context: {
newsContext
}
}
And then we need to create the store, example:
src/store.js
import rootContext from './context'
import { createContext, contextMiddelware } from 'redux-context'
// not sure what this would look like
A parent component/context can always access the child but not the other way around (we want to avoid '../x' dependencies)
Let say we want have two features: news-body and news-list. These two components needs the same
redux action: toggleBody
to hide or show the body text or headline text of a news article.
Folder structure:
src/news-container.jsx
src/news-context.js
src/news-body/news-body-context.js
src/news-body/index.js
src/news-body/news-body-container.jsx
src/news-list/news-list-context.js
src/news-list/index.js
src/news-list/news-list-container.jsx
src/news-container.jsx
import context from './news-context';
import {NewsBodyContainer}, from './news-body';
import {NewsListContainer}, from './news-list';
class NewsComponent extends React.Component {
render() {
return (
<div>
<NewsBodyContainer toggleBody={this.props.toggleBody} bodyVisible={this.props.bodyVisible}/>
<NewsListContainer toggleBody={this.props.toggleBody}/>
</div>
)
}
}
export default connect(context)(NewsComponent);
Now, we need to dispatch the toggleBody
redux action when user clicks on a news list item.
Also assume that we will not use URLs and routing (which would have been the correct impl.) in order to
show how to handle dependencies between two different features.
In the news-context.js
we define
import {newsBodyContext} from './news-body'
import {newsListContext} from './news-list'
// We will now
function toggleBody(id) {
return (dispatch, getState, {toggleBodySuccess}) => {
dispatch(newsBodyContext.actions.getBody()).then(toggleBodySuccess);
}
}
export default {
actions: {
toggleBody,
toggleBodySuccess: () => {type: 'toggleBodySuccess'}
},
reducers: {
bodyVisible: (state=false, action) => (action.type === 'toggleBodySuccess') ? ! state : state
},
contexts: {
newsBody: newsBodyContext,
newsList: newsListContext
}
}
Notice how the state objects mirrors the folder structure. (Not sure if this is always possible to do)
We can then connect the actions and state defined above using the connect
function provided by another library: react-redux-context
.
src/news/component.js
import {connect} from 'react-redux-action-context';
import context from './context';
const NewsBody = (body) => <div>{body}</div>;
const NewsListItem = (headline, getBody) => <div onClick={getBody}>{headline}</div>;
const NewsLists = (newsList) => ...
class NewsContainer extends React.Component {
componentDidMount() { this.props.getNewsList() }
render() {
return (
<div>
<NewsBody newsBody={this.props.newsBody}/>
<NewsList getNewsBody={this.props.getNewsBody} newsList={this.props.newsList}/>
</div>
)
}
}
// connect all props and actions from the context
export default connect(context)(NewsContainer)
Alternative, connect
with mapStateToProps:
import context from './context'
class NewsContainer extends React.Component {
// same as above ...
}
function mapStateToProps(state) {
return {
// Notice, if we did not use the context then we would have to
// write: headers: state.news.newsHeaders where the 'news' object is specified
// in the root reducer.
headers: state.newsHeaders
body: state.newsBody
}
}
export default connect(context, mapStateToProps)(NewsContainer)
Let's compare this without using this library. Is it worth the 'magic' ?
src/news/action-reducers.js
function getBodyPending() {...}
function getBodySucces() {...}
function getBodyError() {...}
function getBody() {
return (dispatch, getState()) {
if (getState().news.newsBody) {
return;
}
dispatch(getBodyPending());
api.getBody().then((data) => dispatch(getBodySucces(data), (data) => dispatch(getBodyFailure(data))
}
}
function newsBody(state, action) {}
function newsHeaders(state, action) {}
export default {
actions: { getBody },
reducers: {newsBody, newsHeaders}
}
src/news/component.js
import {connect} from 'react-redux'
import context from './action-reducers'
const NewsBody = (body) => <div>{body}</div>
// same as above
// connect all props and actions from the context
export default connect(state => state.news, bindActionCreators)(NewsContainer)
- No need to remember where the news state exist in the redux state in
getBody
and the react-redux connect mapStateToProps method. - We can reuse the news (everything in the
./src/news
folder) since it can now exist anywhere in the redux state object. - Easier to read the
getBody
async action creator, since we inject all the action creators for its context - No need to use the dispatch method in the async action creator since it's already bound (not sure this is good, see bindActionCreators