Codementor Events

Implement a Reusable Reducer Logic

Published Mar 06, 2017
Implement a Reusable Reducer Logic

I met a mentee who was working on a project that required him to display two music playlists of different genre on the same page. He didn't want to write two separate reducers for this component as the only difference was the playlist id. He reached out to me but I was unable to help him. I have never had any reason to create a reusable reducer. Although the mentee had access to the Reusing Reducer Logic Examples document, he was still unable to implement these examples in his project. I decided to write this tutorial to help other folks with the same need.

In this tutorial, I will share two implementations of Reusing Reducer Logic Examples with you. I will share them in a step-by-step approach on how to integrate this with your React project.
The first implementation will have a single key in the store with multiple branches while the second will have multiple keys in the store using the same reducer logic.

  // First implementation store structure
 {
  posts:{
    a_posts:{ items:[], isFetching: false},
    b_posts:{ items:[], isFetching: false}
    ...
  }
 }
 
 // Second implementation store structure
 {
  a_posts:{ items:[], isFetching: false},
  b_posts:{ items:[], isFetching: false}
  ...
 }

Audience

This tutorial assumes you are familiar with the concept of Redux. If that's not the case for you, read this Introduction to Redux and Getting Started with React Redux: An Intro first, then come back here to learn how to implement a reusable reducer logic.

What we are building

We will be creating a half-baked blog application that displays posts based on category. We are only implementing posts listing as it's the only functionality that requires a reusable reducer logic.

Environment Configuration

You should have Node installed before proceeding with this tutorial.
You can clone the repo, which is the final outcome of this tutorial, or you can follow it step by step as we iron things out.

Running cloned repo

If you cloned the repo, open your terminal and navigate to the project directory. Install dependencies and start the server. Follow the steps below:

  • cd react-redux-reusable-reducer
  • npm install
  • npm start

The project will compile, start a development server on http://localhost:3000/, and open your browser pointing to that address. You can check out to v2 branch to see the second implementation.

Project skeleton

To get started, we need to initialize a react project skeleton using create-react-app module. If you don't already have create-react-app, just run npm install -g create-react-app in your terminal to install it globally. Next, we need to generate our React app boilerplate. Run the following in your terminal:

  create-react-app react-redux-reusable-reducer
  cd react-redux-reusable-reducer/
  npm start

Open http://localhost:3000 to see your initial project skeleton.

Install Dependencies

We need to install some project dependencies. Feel free to take out isomorphic-fetch if you are not calling external API with fetch. Otherwise, run the following in your terminal:

npm install react-redux redux-thunk isomorphic-fetch --save

Actions

We will start off with the actions for our blog application. Take a look at the actions explanation if you don't already know what actions are.

Under the src directory, create another directory called posts. Inside the posts directory, create a actions.js file with the following content:

src/actions.js

  // import fetch from 'isomorphic-fetch';

  export const REQUEST_POSTS = "FETCH_POSTS";
  export const RECEIVE_POSTS = "RECEIVE_POSTS";

  let sampleResponse = [
    {"title":"test title 1","id":1976235410884491574,"category":"a"},
    {"title":"test title 2","id":3510942875414458836,"category":"a"},
    {"title":"test title 3","id":6263450610539110790,"category":"b"},
    {"title":"test title 4","id":2015796113853353331,"category":"b"}
  ]

  export function requestPosts(name) {
    return {
      type: REQUEST_POSTS,
      name
    }
  }

  export function receivePosts(payload, name) {
    return {
      type: RECEIVE_POSTS,
      name,
      items: payload
    }
  }

  export function fetchPosts(query, name) {

    return dispatch => {
      dispatch(requestPosts(name))
      return new Promise(function(resolve, reject) {
        setTimeout(function() {
          let result = sampleResponse.filter(function(item){
            return item.category === query.cat
          })
          resolve(result);
        }, 1000);
      }).then(payload => dispatch(receivePosts(payload, name)));

      /*
      // You can use this to call an external api endpoint
      return fetch("http://example.com/api?cat=" + query.cat)
        .then(function(response){
           return response.json()
        })
        .then(payload => dispatch(receivePosts(payload, name)))
        */
    }
  }

If you examine the code above, you will notice that all the actions have a name parameter โ€” this will be used to track the component that's dispatching an action.

Note: You do not need to call this parameter name; you can call it whatever you want.

Reducers

It's time to write the reducer logic, which handles how the application's state changes in response to user actions. Read about Reducers if you are wondering what they are and how they work.

Inside posts directory, create a file called reducers.js with the following contents:

src/reducers.js (v1 implementation)


  import {
    REQUEST_POSTS, RECEIVE_POSTS
  } from './actions'

  let singleState = {items: [], isFetching: false};
  let initialState = {
    a_posts : singleState,
    b_posts : singleState,
  }

  export function posts(state = initialState, action) {
    let data = initialState;
    switch (action.type) {
      case REQUEST_POSTS:
        data[action.name].isFetching = true;
        return Object.assign({}, state, data)
      case RECEIVE_POSTS:
        data[action.name].items =  action.items
        data[action.name].isFetching = false;
        return Object.assign({}, state, data)
      default:
        return state
    }
  }

In the reducer logic above, the initial state of each component will be stored in their own key.

  let initialState = {
      a_posts : singleState,
      b_posts : singleState,
      c_posts : singleState,
      ...
    }

You can decide to add more to it as shown above. Also, note that the key name is the same name we dispatched in the actions. The new state is computed and returned into the corresponding key based on the name dispatched by the actions.

Now, let's take a look at the second implementation.

src/reducers.js (v2 implementation)

  import {
    REQUEST_POSTS, RECEIVE_POSTS
  } from './actions'

  let initialState = {items: [], isFetching: false};

  export function postWithName(name) {
    return function posts(state = initialState, action) {
      if(name !== action.name)
        return state;

      switch (action.type) {
        case REQUEST_POSTS:
          return Object.assign({}, state, {isFetching: true})
        case RECEIVE_POSTS:
          return Object.assign({}, state, {isFetching: false, items: action.items})
        default:
          return state
      }
    }
  }

The postWithName function returns the inner function (the actual reducer logic) and the name parameter is used to compare the name in the action dispatched. If the name is not equal, the current state is returned. See other implementations here.

Store

The store holds all application state. Read more about Store here if you don't already know.

We will implement the store logic in the entry file index.js. Open your current index.js and replace it with the following code:

src/index.js (v1 implementation)

  import React from 'react';
  import thunkMiddleware from 'redux-thunk';
  import { render } from 'react-dom';
  import { Provider } from 'react-redux';
  import { createStore, applyMiddleware } from 'redux';
  import { posts } from './posts/reducers';
  import App from './App';


  function storeWrapper(state = {}, action) {
    return {
      posts: posts(state.posts, action)
    }
  }

  let store = createStore(
    storeWrapper,
    applyMiddleware(
        thunkMiddleware
      )
    )

  render(
    <Provider store={store}>
      <App />
    </Provider>,
    document.getElementById('root')
  )

Nothing special here, only one key posts. This will hold other keys as specified in the reducer logic, each key with it own state. Let's take a look at v2โ€™s implementation.

src/index.js (v2 implementation)

  import React from 'react';
  import thunkMiddleware from 'redux-thunk';
  import { render } from 'react-dom';
  import { Provider } from 'react-redux';
  import { createStore, applyMiddleware } from 'redux';
  import { postWithName } from './posts/reducers';
  import App from './App';


  function storeWrapper(state = {}, action) {
    return {
      a_posts: postWithName('a_posts')(state.a_posts, action),
      b_posts: postWithName('b_posts')(state.a_posts, action)
    }
  }

  let store = createStore(
    storeWrapper,
    applyMiddleware(
        thunkMiddleware
      )
    )

  render(
    <Provider store={store}>
      <App />
    </Provider>,
    document.getElementById('root')
  )

The only difference here is in the storeWrapper function. The postWithName function will return the actual reducer logic. I guess you probably figured out why we used double parenthesis ๐Ÿ˜‰

return {
    a_posts: postWithName('a_posts')(state.a_posts, action),
    b_posts: postWithName('b_posts')(state.a_posts, action)
  }

The first parenthesis calls the postWithName function with a name for the component, which will be able to modify the state, while the second parenthesis calls the returned reducer logic with the current state and action.

Note: It's not compulsory that the key name will be the same name passed to postWithName function.

Display Components

We are finished with the hard part. Let's create a component that will display the posts. Create a directory under the posts directory we created earlier โ€” this new directory will be called components. Paste the following code into a file called posts.js in the newly created directory.

src/posts/components/post.js

  import React, { PropTypes, Component } from 'react'

  export default class Posts extends Component {
    render() {
      return (
        <ul>
          {this.props.posts.map((post, i) =>
            <li key={i}>{post.title} ({post.category}) </li>
          )}
        </ul>
      )
    }
  }

  Posts.propTypes = {
    posts: PropTypes.array.isRequired
  }

Display Container

The container will serve as a data source for our display component. This article explains it well. Create another directory containers inside posts directory and add the following code into PostList.js.

src/posts/containers/PostList.js (v1 implementation)

  import React, { Component, PropTypes } from 'react'
  import { connect } from 'react-redux'
  import { fetchPosts } from '../actions'
  import Posts from '../components/posts'

  class PostContainer extends Component {

    componentDidMount() {
      const { dispatch} = this.props
      dispatch(fetchPosts({cat:this.props.cat}, this.props.name))
    }

    render() {
      const { posts, isFetching } = this.props
      return (

        <div>
        {isFetching &&
            <div>
              Loading...
            </div>
          }
          {posts.length > 0 &&
            <div style={{ opacity: isFetching ? 0.5 : 1 }}>
              <Posts posts={posts} />
            </div>
          }
        </div>
      )
    }
  }

  PostContainer.propTypes = {
    posts: PropTypes.array.isRequired,
    cat: PropTypes.string.isRequired,
    isFetching: PropTypes.bool.isRequired,
    name: PropTypes.string.isRequired,
    dispatch: PropTypes.func.isRequired
  }

  function mapStateToProps(state, props) {
    return {
      posts: state.posts[props.name].items,
      isFetching: state.posts[props.name].isFetching,
    }
  }

  export default connect(mapStateToProps)(PostContainer)

src/posts/containers/PostList.js (v2 implementation)

  import React, { Component, PropTypes } from 'react'
  import { connect } from 'react-redux'
  import { fetchPosts } from '../actions'
  import Posts from '../components/posts'

  class PostContainer extends Component {

    componentDidMount() {
      const { dispatch} = this.props
      dispatch(fetchPosts({cat:this.props.cat}, this.props.name))
    }

    render() {
      const { posts, isFetching } = this.props
      return (

        <div>
        {isFetching &&
            <div>
              Loading...
            </div>
          }
          {posts.length > 0 &&
            <div style={{ opacity: isFetching ? 0.5 : 1 }}>
              <Posts posts={posts} />
            </div>
          }
        </div>
      )
    }
  }

  PostContainer.propTypes = {
    posts: PropTypes.array.isRequired,
    cat: PropTypes.string.isRequired,
    isFetching: PropTypes.bool.isRequired,
    name: PropTypes.string.isRequired,
    dispatch: PropTypes.func.isRequired
  }

  function mapStateToProps(state, props) {
    return {
      posts: state[props.name].items,
      isFetching: state[props.name].isFetching,
    }
  }

  export default connect(mapStateToProps)(PostContainer)

The difference in the two versions of code above is the mapStateToProps functions. If you take a closer look at the returned object, you will notice that the first implementation receives values from state.posts[props.name] while the second receives a value from state[props.name].

Final Step

It's time to connect the final piece of our blog posts application and, finally, test the result. In the root folder, replace App.js contents with the following code:

src/App.js

  import React from 'react'
  import PostList from './posts/containers/PostList'

  const App = () => (
    <div>
      <h3>Posts - Category A</h3>
      <PostList cat="a" name="a_posts"/>
      <h3>Posts - Category B</h3>
      <PostList cat="b" name="b_posts" />
    </div>
  )

  export default App

In the code above, the PostList is used twice with two props cat and name. cat is the value to use in filtering the incoming posts while name is used to identify the component dispatched actions.

If you still have your server running, you should see the result in your browser. You can run npm start in your terminal from the project root folder if your server is not up.

Result Screenshot

Screen Shot 2017-02-23 at 9.48.59 PM.png

Final Note

I hope this tutorial has been helpful, You can fork the repo and feel free to contribute if you have other implementations.

Discover and read more posts from Sunday Nwuguru
get started