Best practices with redux, flow type, async/await and a few extra tips

(There are some formatting issues in this post, will be fixed soon)
This is an alternative way to use Redux by reducing some boilerplate and using flow.
This will focus more on the Redux architecture and not much on the components and flow.

Benefits:

  • Cleaner action dispatchers (no callbacks);
  • Less boilerplate (No need for mapDispatchToProps);
  • Easier to debug and follow the code (No need for thunks and middlewares);

Folder structure:

  • src/actions/creators: It just creates an action object (no action dispatching here);
  • src/actions/dispatchers: It just dispatches an action object (no action creation here);
  • src/reducers: Contains all reducers (pure functions as always);

This is different than standard Redux because we separate between creators and dispatchers.

The App component

This will be at the very top of your react hierarchy.

// @flow
import React from 'react';
import {Provider} from 'react-redux';

class App extends React.Component {
  // Defines what context shape the children will have.
  static childContextTypes = {
    dispatch: React.PropTypes.func.isRequired,
  };

  props: {
    // We receive the store from the parent.
    store: Object,
  };

  getChildContext() {
    return {
      // Adds the implementation for the `dispatch` function.
      dispatch: (actionDispatcher: Function, data: any) => {
        // Here we just call the `actionDispatcher` with the data and the `store.dispatch`.
        actionDispatcher(data, this.props.store.dispatch);
      },
    };
  }

  render() {
    const {store, children} = this.props;
    // Standard redux wrapper with `<Provider/>`.
    return (
      <Provider store={store}>{children}</Provider>
    );
  }
}

export default App;

The app will provide a function named dispatch to the context of every child so it can be used later on (it will be explained how to use it below).

The action dispatcher

src/actions/dispatchers.js

// @flow
import api from '../api';
import type {Dispatch} from '../types/dispatch';
import actionCreators from './creators';

const actionDispatchers = {
  user: {
    get: async ({id}: {id: string}, dispatch: Dispatch) => {
      // Here we dispatch the first action to let the app know that we are loading the user.
      dispatch(actionCreators.user.get.loading());
      // Because we are using `async/await` we can just wait here for the api to return the data.
      // For the simplicity's sake we don't handle the failure scenario. We could just add a simple `try/catch`.
      const user = await api.getUser({id});
      // Here we dispatch to let know that app that we got a new user. The reducers will take care
      // of inserting the new user in the store.
      dispatch(actionCreators.user.get.complete({user}));
    },
  },
};

export default actionDispatchers;

Here the action dispatcher receives the data and the dispatch function. Uses the action creator to create the function.

Notice that we can have async functions and there are no need for thunks.

The code with thunks would require an extra wrapper function that makes debugging more complicated and is unnecessary. Also there is no need for a middleware.

Below you could see the standard redux action dispatcher:

// @flow
import api from '../api';
import type {Dispatch} from '../types/dispatch';
import actionCreators from './creators';

const actionDispatchers = {
  user: {
    // Here we have a sync function and an async function nested. You would call them like this `await get({id: 'myId'})(dispatch);`
    // Of course in reality you won't call it like this because the thunk middleware will make the second call `(dispatch);`.
    get: ({id}: {id: string}) => async (dispatch: Dispatch) => {
      dispatch(actionCreators.user.get.loading());
      const user = await api.getUser({id});
      dispatch(actionCreators.user.get.complete({user}));
    },
  },
};

export default actionDispatchers;

Did you noticed the nested function? init: (data: void) => async (dispatch: Dispatch) => { – Is not really necessary at all.

On top of it we dispatch an action that is a function that dispatches an action that is

The action creator

src/actions/creators.js

// @flow
import type {User} from '../reducers';

const actionTypes = {
  user: {
    get: {
      loading: `user.get.loading`,
      complete: `user.get.complete`,
    },
  },
};
export {actionTypes};

// Defines the flow Action types
export type Action = ReduxInit | UserGetLoading | UserGetComplete;
export type ReduxInit = {
  type: '@@INIT',
}
export type UserGetLoading = {
  type: 'user.get.loading',
}
export type UserGetComplete = {
  type: 'user.get.complete',
  user: User,
}

export default {
  user: {
    get: {
      loading(): UserGetLoading {
        return {
          type: actionTypes.user.get.loading,
        };
      },
      complete({user}: {user: User}): UserGetComplete {
        return {
          type: actionTypes.user.get.complete,
          user,
        };
      },
    },
  },
};

Nothing exciting going on here, just some functions that creates some objects.

The most annoying issue is that we need to duplicate the action types strings between the javascript object and the flow type.

A component

src/components/User/User.js

// @flow
import React from 'react';
import {connect} from 'react-redux';
// `getUser` is a selector
import {getUser} from '../../reducers';
import actionDispatchers from '../../actions/dispatchers';

class User extends React.Component {
  static contextTypes = {dispatch: React.PropTypes.func.isRequired};

  componentWillMount() {
    // Here we just provide the function to the `dispatch`. Note that we don't call the action dispatcher here.
    // The `dispatch` function will invoke `actionDispatchers.user.get` with the data provided and the `dispatch` method from the store. 
    this.context.dispatch(actionDispatchers.user.get, {id: `myId`});
  }

  render() {
    return (
      <div>User id: {this.props.user.id}</div>
    );
  }
}

const mapStateToProps = state => ({
  // Use of selector to get the user as a prop.
  user: getUser(state),
});

export default connect(mapStateToProps)(User);

Here are the most important changes:

  • mapDispatchToProps is not required anymore;
  • Actions are dispatched by calling this.context.dispatch;

Reducers and selectors

Will remain the same as standard Redux.

Conclusions

We can do even better than this, we could add a middleware that adds the dispatch argument on the action dispatcher.

Overall it seems that we can do complex stuff in Redux thunks.

If you find it useful, please retweet.
Thanks for reading.