Redux Ducks: Restructure your Redux App with Ducks

This tutorial is part 2 of 2 in the series.

Part 1:

The Redux Ducks: Restructure your Redux App with Ducks tutorial will teach you how to bundle action creators, action types and reducers side by side in your Redux app.

Usually in the beginning of learning Redux you have a technical separation of concerns which gets reflected in the folder structure. Basically there is one folder for your actions and one folder for your reducers. Additionally you collect all action types at one place that they can be reused by reducers and actions.

Since it is often the case that an action is followed by a reducer and both of them share the same action type, a new recommendation came up to collocate all of them at one place. They call it Ducks.

The tutorial itself will not strictly follow all proposed guidelines of the recommendation, but it gives you a good understanding of how your app would look like after the refactoring and the advantages of using the bundling.

Seed Project?

If you don’t have the outcome of The SoundCloud Client in React + Redux, checkout this repository. Clone the repository and use the Base folder named base-init/ as seed project. The seed project is the same which you would implement when you would step through the initial tutorial. Next to the Base folder you will find several Extension folders, which will show the outcome of each tutorial which builds up on The SoundCloud Client in React + Redux.

When you have your own The SoundCloud Client in React + Redux project, you can continue with the Table of Contents and ignore the following instructions.

When you want to use the seed project, make sure to clone the repository. Use the base-init/ folder as your project folder. Make sure that your project starts with npm install && npm start.

Table of Contents

Refactor Auth Redux Duck

Basically we have two ducks in the SoundCloud app: There is one place for the authentication and data fetching and another place where the tracks are saved and played.

Let’s begin with the auth duck: In the existent app you will find the auth actions in src/actions/auth.js and the reducer in src/reducers/auth.js. Moreover there is one action type in the src/constants/actionTypes.js file.

A new folder for the ducks instead of actions / reducers folder pairs will help us to collocate actions and reducers.

From src folder:

mkdir ducks
cd ducks
touch auth.js

At first we can move the the sole auth action type.

src/ducks/auth.js

const ME_SET = 'ME_SET';

As you can see we are not exporting anything at this time. We can even more refactor the action type itself to represent the duck bundle. In a growing application it is an improved way to identify the actions and places in the source code.

src/ducks/auth.js

const ME_SET = 'auth/ME_SET';

The next step is to move the action creators. I have highlighted the important pieces after the copy and paste from src/actions/auth.js.

src/ducks/auth.js

import SC from 'soundcloud';
import { setTracks as doSetTracks } from '../actions';

const ME_SET = 'auth/ME_SET';

function doSetMe(user) {
  return {
    type: ME_SET,
    user
  };
}

function doAuth() {
  return function (dispatch) {
    SC.connect().then((session) => {
      dispatch(doFetchMe(session));
      dispatch(doFetchStream(session));
    });
  };
}

function doFetchMe(session) {
    return function (dispatch) {
      fetch(`//api.soundcloud.com/me?oauth_token=${session.oauth_token}`)
        .then((response) => response.json())
        .then((data) => {
          dispatch(doSetMe(data));
        });
    };
}

function doFetchStream(session) {
  return function (dispatch) {
    fetch(`//api.soundcloud.com/me/activities?limit=20&offset=0&oauth_token=${session.oauth_token}`)
      .then((response) => response.json())
      .then((data) => {
        dispatch(doSetTracks(data.collection));
      });
  };
}

Again we are not exporting anything. Moreover the action creators got prefixed. Since reducers and action creators will live side by side, it is a good way to keep the naming in your file tidy. Additionally we need to import the action creator to set tracks like we did before, but with an alias to follow the new naming convention. We will refactor that later when we have a duck for the track bundle as well.

Last but not least let’s move our reducer.

src/ducks/auth.js

import { CLIENT_ID, REDIRECT_URI } from '../constants/auth';
import { setTracks as doSetTracks } from '../actions';

const ME_SET = 'auth/ME_SET';

function doSetMe(user) {
  ...
}

function doAuth() {
  ...
}

function doFetchMe(session) {
  ...
}

function doFetchStream(me, session) {
  ...
}

const initialState = {};

function reducer(state = initialState, action) {
  switch (action.type) {
    case ME_SET:
      return applySetMe(state, action);
  }
  return state;
}

function applySetMe(state, action) {
  const { user } = action;
  return { ...state, user };
}

Note that the reducer is a named function and we prefixed its functions as well. As last step we have to export all the necessary stakeholders.

src/ducks/auth.js

...

const actionCreators = {
  doAuth
};

const actionTypes = {
  ME_SET
};

export {
  actionCreators,
  actionTypes
};

export default reducer;

Usually you don’t need to export the action types, but there may be cases where you have to export. They could be used by tests or other side effect middleware like redux-saga. The example just gives a suggestion how you would accomplish a clean export of all stakeholders.

Now it is time to clean up and remove the files which are unused.

From src folder:

rm actions/auth.js
rm reducers/auth.js

Remove the unused action type ME_SET as well. Keep the remaining action types for now.

src/constants/actionTypes.js

export const TRACKS_SET = 'TRACKS_SET';
export const TRACK_PLAY = 'TRACK_PLAY';

Moreover we can remove the dependency in the entry points of our legacy actions. The file should look like the following without the auth bundle:

src/actions/index.js

import { setTracks, playTrack } from './track';

export {
  setTracks,
  playTrack
};

After the auth duck is finished and all actions creators and reducers are side by side, we can use the new reducer location to export the combined reducers for the store and use the replaced action creators in the Stream container.

src/reducers/index.js

import { combineReducers } from 'redux';
import { routerReducer } from 'react-router-redux';
import auth from '../ducks/auth';
import track from './track';

export default combineReducers({
  auth,
  track,
  routing: routerReducer
});

src/components/Stream/index.js

import React from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import * as actions from '../../actions';
import { actionCreators as authActionCreators } from '../../ducks/auth';
import Stream from './presenter';

function mapStateToProps(state) {
  const { user } = state.auth;
  const { tracks, activeTrack } = state.track;
  return {
    user,
    tracks,
    activeTrack
  }
}

function mapDispatchToProps(dispatch) {
  return {
    onPlay: bindActionCreators(actions.playTrack, dispatch),
    onAuth: bindActionCreators(authActionCreators.doAuth, dispatch)
  };
}

export default connect(mapStateToProps, mapDispatchToProps)(Stream);

The app should still be intact after all, but it comes with our first duck!

From root folder:

npm start

Refactor Redux Track Duck

Now it’s time to create the track duck.

From ducks folder:

touch track.js

Let’s move the action types, action creators and reducer. Again I highlighted the changed pieces after copy and pasting the relevant lines of code.

src/ducks/track.js

const TRACKS_SET = 'track/TRACKS_SET';
const TRACK_PLAY = 'track/TRACK_PLAY';

function doSetTracks(tracks) {
  return {
    type: TRACKS_SET,
    tracks
  };
};

function doPlayTrack(track) {
  return {
    type: TRACK_PLAY,
    track
  };
}

const initialState = {
  tracks: [],
  activeTrack: null
};

function reducer(state = initialState, action) {
  switch (action.type) {
    case TRACKS_SET:
      return applySetTracks(state, action);
    case TRACK_PLAY:
      return applySetPlay(state, action);
  }
  return state;
}

function applySetTracks(state, action) {
  const { tracks } = action;
  return { ...state, tracks };
}

function applySetPlay(state, action) {
  const { track } = action;
  return { ...state, activeTrack: track };
}

const actionCreators = {
  doSetTracks,
  doPlayTrack
};

const actionTypes = {
  TRACKS_SET,
  TRACK_PLAY
};

export {
  actionCreators,
  actionTypes
};

export default reducer;

Now we provide the store with the relocated reducer like we did with the auth duck.

src/reducers/index.js

import { combineReducers } from 'redux';
import { routerReducer } from 'react-router-redux';
import auth from '../ducks/auth';
import track from '../ducks/track';

export default combineReducers({
  auth,
  track,
  routing: routerReducer
});

Same applies for the Stream container component. We can import the actionCreators from their new place.

src/components/Stream/index.js

import React from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { actionCreators as trackActionCreators } from '../../ducks/track';
import { actionCreators as authActionCreators } from '../../ducks/auth';
import Stream from './presenter';

function mapStateToProps(state) {
  const { user } = state.auth;
  const { tracks, activeTrack } = state.track;
  return {
    user,
    tracks,
    activeTrack
  }
}

function mapDispatchToProps(dispatch) {
  return {
    onPlay: bindActionCreators(trackActionCreators.doPlayTrack, dispatch),
    onAuth: bindActionCreators(authActionCreators.doAuth, dispatch)
  };
}

export default connect(mapStateToProps, mapDispatchToProps)(Stream);

Remember when we had to import the setTracks as doSetTracks alias in the auth duck? Now we renamed it due to the track duck refactoring and can change that in the auth duck.

src/ducks/auth.js

import SC from 'soundcloud';
import { actionCreators as trackActionCreators } from './track';

const ME_SET = 'auth/ME_SET';

function doSetMe(user) {
  return {
    type: ME_SET,
    user
  };
}

function doAuth() {
  ...
}

function doFetchMe() {
  ...
}

function doFetchStream(session) {
  return function (dispatch) {
    fetch(`//api.soundcloud.com/me/activities?limit=20&offset=0&oauth_token=${session.oauth_token}`)
      .then((response) => response.json())
      .then((data) => {
        dispatch(trackActionCreators.doSetTracks(data.collection));
      });
  };
}

const initialState = {};

...

As last step we can remove all unused folders and files now.

From src folder:

rm -rf actions
rm constants/actionTypes.js
rm reducers/track.js

After the refactoring the folder structure should look like the following:

Folder structure:

-src
--components
--constants
--ducks
---auth.js
---track.js
--reducers
--stores
--index.js

Finally we have a clean bundling of {action type, action creator and reducer} tuples with the ducks pattern. We still have a reducers folder to combine all of the reducers for the store, but one could move this next to the store to get rid of the reducers folder as well. After that the app would only have components and ducks as main bundling folders.

Troubleshoot

You may encounter issues in that tutorial. Here you will find some references how to handle issues.

Dependencies

In case you want to know which versions npm installed during that tutorial, here a list of all npm packages in my package.json.

  "devDependencies": {
    "babel-core": "^6.9.1",
    "babel-loader": "^6.2.4",
    "babel-preset-es2015": "^6.9.0",
    "babel-preset-react": "^6.5.0",
    "babel-preset-stage-2": "^6.5.0",
    "chai": "^3.5.0",
    "enzyme": "^2.3.0",
    "exports-loader": "^0.6.3",
    "imports-loader": "^0.6.5",
    "jsdom": "^9.2.1",
    "mocha": "^2.5.3",
    "react-addons-test-utils": "^15.1.0",
    "react-hot-loader": "^1.3.0",
    "webpack": "^1.13.1",
    "webpack-dev-server": "^1.14.1"
  },
  "dependencies": {
    "react": "^15.1.0",
    "react-dom": "^15.1.0",
    "react-redux": "^4.4.5",
    "react-router": "^2.4.1",
    "react-router-redux": "^4.0.5",
    "redux": "^3.5.2",
    "redux-logger": "^2.6.1",
    "redux-thunk": "^2.1.0",
    "soundcloud": "^3.1.2",
    "whatwg-fetch": "^1.0.0"
  }

Final Thoughts

Have a look again in The SoundCloud Client in React + Redux tutorial for more smaller tutorials which build up on top of the SoundCloud project. All tutorials on top will get released in the repository as well.

Build a Hacker News App along the way. No setup configuration. No tooling. No Redux. Plain React in 170+ pages of learning material. Learn React like 9000+ readers.

Get the Book
comments powered by Disqus

Never miss an article about web development and self-growth.

Take Part

Join 5000+ Developers

Learn Web Development

Tips and Tricks

Access Tutorials, Books and Courses

Personal Development as a Software Engineer