Skip to content

Instantly share code, notes, and snippets.

@andreasronge
Last active October 27, 2015 18:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save andreasronge/48f711513480b216d254 to your computer and use it in GitHub Desktop.
Save andreasronge/48f711513480b216d254 to your computer and use it in GitHub Desktop.
Trying to improve redux architecture - cohesion and state context

Redux-Context

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 the react-redux connect method).
npm install --save redux-context

How ?

Better Cohesion

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) => {},
  }
}

Create a tree of context object

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 

Using child context actions

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)

Connect to Redux properties

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)

Can we solve these problems without using another library ?

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)

Advantages

  • 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment