Getting Started with Redux: Part 3 – Refactoring

We create open-source because we love it, and we share our finding so everyone else can benefit as well.

Getting Started with Redux: Part 3 – Refactoring

Now that we have a working Redux app now, we should start looking at ways we can optimize our code, as well as take full advantage of redux. In this last part of getting started with redux, we will cover how to take advantage of asynchronous actions, DRYing up our code, and controlling and accessing redux anywhere in our app.

Part 1: Beginner’s Guide to Redux – About

Part 2: Getting Started with Redux – Redux App

05 Making Actions Even Better

branch: 05-scalable

Getting started, we should start to focus on how to add asynchronous actions. If you haven’t already noticed, the actions we have now are purely synchronous, in the sense they do one thing, and they are done. In many cases we want asynchronous actions, rather to wait for another action, or wait for an API to finish processing.

If you remember from Part 2, we already added the thunk middleware, which allows us to use asynchronous actions. All we need to do now is make some asynchronous actions for the middleware to process, and moving our Github User actions to Redux would make for a great example of this. First off, let’s add the following to the bottom of our ActionTypes file:

// users
export const USER_FETCH_START = 'USER_FETCH_START';
export const USER_FETCH_SUCCESS = 'USER_FETCH_SUCCESS';
export const USER_PROGRESS = 'USER_PROGRESS';

As you can already see, we are adding two constants for a single action. The reason we need two is one for the start of the action, and another for when the process has finished, so we always know the action has in fact finished. Let’s see what this looks like as an action in our new UsersAction.js file:

import * as types from './ActionTypes'
import fetch from 'node-fetch'

export function userFetch() {
  return dispatch => {
  dispatch(userFetchStart());
  dispatch(progressChange(20));
    fetch('https://api.github.com/users')
    .then(res => res.json()).then(result => {
      dispatch(progressChange(100));
      dispatch(userFetchSuccess(result));
    }).catch(error => {
      dispatch(callError(error));
    })
  }
}

export function userFetchStart() {
  return { type: types.USER_FETCH_START }
}

export function userFetchSuccess(data) {
  return { type: types.USER_FETCH_SUCCESS, data }
}

export function progressChange(data) {
  return { type: types.USER_PROGRESS, data }
}

export function callError(error) {
  return { type: types.CALL_ERROR, error }
}

Since we haven’t touched this action yet, let’s first explain what we are accomplishing with this action. We have two main actions, the API fetch which is pointed towards the Github users namespace, and then the progress change, which controls the progress bar on the frontend.

Adding this to redux isn’t a drastic change, but let’s go through the process of this action line by line starting with the userFetch function on line 4:

line 5: To start we are passing the dispatch function to our action. This allows us to “dispatch” actions, like our usual synchronous actions

line 6-7: With the action started, we should let the app know by dispatching the userFetchStart function, and then dispatching progressChange to update the progress bar to 20%

line 8: Now we call our internal action/API, and in this case fetching Github Users

line 9: Since fetch is promise based, we wait for a reply, and when returned we parse to json, and return the result

line 10-11: Once the result is received we dispatch the progressChange function to show the action has finished on the frontend, and then dispatch the FetchSuccess function to let the app know too

line 13: If for any reason our promise fails, or an error is captured within our action, we then dispatch the callError with the error.

Overall you can see we are doing nothing more than encapsulating an asynchronous action in a collection of synchronous actions, which make the calls to the reducer. To be honest there isn’t a lot to it, other than the ability to make the app aware when asynchronous actions are running, or processed.

Let’s now see how to handle state with these asynchronous actions, because there is more we can take from this. Let’s go ahead and create a new reducer called UserReducer.js:

import * as types from '../actions/ActionTypes'
import initialState from './initialState';

export default function userReducer(state = initialState.users, action) {
  switch(action.type) {
    case types.USER_FETCH_START:
      return {
        ...state, loadingList: true, progress: 20
      }
    case types.USER_FETCH_SUCCESS:
      return {
        ...state, loadingList: false, userList: action.data
      }
    case types.USER_PROGRESS:
      return {
        ...state, progress: action.data
      }
    case types.CALL_ERROR:
      return {
        ...state, loadingList: false, progress: 0, userList: []
      }
    default:
      return state;
  }
}

Don’t forget to update your rootReducer as well:

 const rootReducer = combineReducers({
  counter: CounterReducer,
  users: UserReducer
})

Compared to our CounterReducer, you may notice quite a bit more state being passed around, but why? Since asynchronous actions revolve on the state of the action, we also want to offer this ability to the rest of the app. For instance, our USER_FETCH_START object passes a loadingList state true, as well as a progress state set to 20. Looking at the USER_FETCH_SUCCESS we see the loadingList set to false, and our userList populated. The reasoning behind this is that we want the ability for the app to know what the action is doing, and the current state. When another action’s ability to be called hangs on the state of an action being processed, versus running, this is exactly the type of state we need.

To finish talking about the reducer let’s look at the error call. If you look at the state, you’ll notice it does nothing more than reset the namespace state. This is a great concept to have on your app, as you can use it with multiple actions in each namespace, since they work on the same state. So any time an error is caught we simply reset the state for that namespace, and in turn returning us to a clean state which we can always revert via the DevTools if ever needed.

With the actions setup we need to setup the frontend, starting by removing the old state and getUsers action leaving only a constructor and render function. We then need to add the actions to the App.js file:

import * as CounterActions from '../actions/CounterActions'
import * as UserActions from '../actions/UserActions'

...

function mapStateToProps(state) {
  return {
    state: {
      counter: state.counter,
      users: state.users
    }
  }
}

function mapDispatchToProps(dispatch) {
  return {
    actions: {
      counter: bindActionCreators(CounterActions, dispatch),
      users: bindActionCreators(UserActions, dispatch)  
    }
  }
}

You may notice a difference in the mapping of the states, and dispatch. Instead of dedicating them to a single state, we can instead map the state, and actions props to hold multiple namespaces. With this change we can now access our state with this.props.state.namespace, and this.props.actions.namespace respectively. Let’s now add our actions and state to the JSX:

render() {
  return (
    <div className="container"> 
      <Text
        text={this.props.state.counter.title}
        className="body"
      />
      <div className="body counter"> 
        <Text
          text={this.props.state.counter.total}
          className="counter-number"
        />
        <Button
          text="+"
          className="button btn-light"
          onClick={() => this.props.actions.counter.counterUp()}
        />
        <Button
          text="-"
          className="button btn-light"
          onClick={() => this.props.actions.counter.counterDown()}
        /> 
      </div>
      <div className="get-user-buttons">
        <Button
          text="Get Users"
          className="button" onClick={() =>  this.props.actions.users.userFetch()} 
        />
      </div>
      {this.props.state.users.progress ?
        <ProgressBar progress={this.props.state.users.progress} />
      : ''}
      <div className="user-container">
        <UserList list={this.props.state.users.userList} />
      </div>
    </div>
  )
}

Basically we’ve switched out our this.state with this.props.state.users, and this.getUsers to this.props.actions.users.getUsers. After that, we have completely converted the React App to a React/Redux App.

06 DRY’ing things Up

branch: 06-drying

In reality we are never done making our apps better, so let’s look at how to DRY up our app. The first one is a controversial change, the use of constants for action calls. We can actually get rid of the ActionTypes, and instead pass strings in our synchronous functions:

 export function counterUp() {
  return {type: 'COUNTER_UP' }
}

export function counterDown() {
  return {type: 'COUNTER_DOWN' }
}

This also means we need to change the reducer to use strings as well:

 export default function (state = initialState.counter, action) {
  switch(action.type) {
    case 'COUNTER_UP':
      return {
        ...state, total: state.total + 1
      }
    case 'COUNTER_DOWN':
      return {
        ...state, total: state.total - 1
      }
    default:
      return state;
  }
}

This is a good way to DRY up our code, but I personally prefer the constant practice. When using constants, we always reference the same constant across actions, reducers, and even middleware. If we use the string method, and make a typo in an action in one of these areas, it will break the action, whereas with constants it will just be a mis-spelled action.

Next we can remove the need for the configureStore file by moving the store to the index itself.

...
import { createStore, applyMiddleware, compose } from 'redux'
import DevTools from './containers/DevTools'
import thunkMiddleware from 'redux-thunk'
import rootReducer from './reducers/rootReducer'

const store = createStore(
  rootReducer,
  compose(
    applyMiddleware(
      thunkMiddleware
    ),
    DevTools.instrument()
  )
);

render(
  <Provider store={store}>
    <ErrorBoundary>
      <App />
      <DevTools />
    </ErrorBoundary>
    </Provider>,
  document.getElementById('app')
);

This is a minor change, but a decent change to make for our smaller apps. When our store takes more responsibilities, like a large amount of custom middleware, we can move it back. You will find that isometric layouts do a lot of changes like this, where you will find the redux components all over the place, instead of in specific folders. If you find yourself using redux in smaller apps, this is a great practice to look into.

This next one is more of javascript refactor practice, but a great one for redux and react, simply due to the amount of objects imported and exported within our app. The change is a nice idiom for exporting nameless objects. Since we are naming the object on import, we really do not need to name them on export. Let me give you an example with our CounterReducer:

 export default function (state = initialState.counter, action) {
  switch(action.type) {
    case 'COUNTER_UP':
      return {
        ...state, total: state.total + 1
      }
    case 'COUNTER_DOWN':
      return {
        ...state, total: state.total - 1
      }
    default:
      return state;
  }
}

There are lots of other places we can DRY things up, but realistically the majority of those practices would be classified as idiomatic redux idioms. Again this is all personal preference, where in my case there resides some sort of borderline OCD, wanting everything in its own place.

07 Props Everywhere

branch: 07-centralized

Since our state and actions are added to props, we can pass props to any component and access them in the components we pass to. Just like any other React app we can continue to pass these props down the tree, not only removing the need to map our state and actions to other components, but just like normal react actions we can use our redux actions in these components to change the state. While there are some anti-patterns involved, we can also access our state and actions anywhere in redux. Right now I just want you to think about that, but when we get to the end we’ll see this in practice.

One particular feature I want to bring up at this time, is that we can pass actions across namespaces. In this section we’re going to create our own Error namespace for our errors, but also making use of this ability to pass actions across namespaces. Let’s start with our UserActions by adding two new functions createError, and userError:

...
      dispatch(userFetchSuccess(result));
    }).catch(error => {
      dispatch(createError(error.message));
    })
  }
}

...

export function createError(error) {
  return dispatch => {
    dispatch(userError())
    dispatch(callError(error))
  }
}

export function userError() {
  return { type: 'CALL_USER_ERROR' }
}

export function callError(error) {
  return { type: 'CALL_ERROR', error }
}

As you can already see, we have two new error functions. The userError is for our User namespace, and the createError sends both the userError, and callError actions. The idea is to send the reset error to the User namespace, and the error itself to another action, which we will move to a new namespace. Let’s update the UserReducer first:

...

    case 'USER_PROGRESS':
      return {
        ...state, progress: 100
      }
    case 'CALL_USER_ERROR':
      return {
        ...state, loadingList: false, progress: 0, userList: []
      }
    default:
      return state;
  }
}

Now that that reducer is taken care of, let’s add the new namespace:

 const initialState = {
  counter: {
    total: 0,
    title: 'Our Redux App'
  },
  users: {
    progress: 0,
    loadingList: false,
    userList: []
  },
  errors: {
    hasError: false,
    message: ''
  }
}

export default initialState

Next the rootReducer:

 import { combineReducers } from 'redux'
import CounterReducer from './CounterReducer'
import UserReducer from './UserReducer'
import ErrorReducer from './ErrorReducer'

const rootReducer = combineReducers({
  counter: CounterReducer,
  users: UserReducer,
  errors: ErrorReducer
})

export default rootReducer

and lastly the ErrorReducer itself:

import initialState from './initialState'

export default function (state = initialState.errors, action) {
  switch(action.type) {
    case 'CALL_ERROR':
      return {
        ...state, hasError: true, message: action.error
      }
    default:
      return state;
  }
}

Everything we need is here. As you may notice, we don’t have an ErrorActions, and we really don’t need it since we are making the error call from the Users namespace. Once we dispatch callError function, this reducer will intercept it, can finish the process, but why go to all this trouble you may ask? Keeping errors centralized can be extremely helpful, meaning any time an error is thrown, you can intercept every single error in the app with a single function, as well as only changing a single state in the app.

In my own apps I have an error display that is handled using this practice, so any time an error is throw in any of my 10+ namespaces, it will always shows on that same display and presented the same way every time.

To show this, let’s update the ErrorBoundary to use this concept:

 import React, { Component, Fragment } from 'react';
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import * as ErrorActions from '../actions/ErrorActions'

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
  }

  componentDidCatch(error, info) {
    this.props.actions.callError(info);
  }

  render() {
    if(this.props.state.hasError) {
      return (
        <div>
          <h2>Something went wrong</h2>
          <details style={{ whiteSpace: 'pre-wrap' }}>
            {this.props.state.message}
          </details>
        </div>
      );
    }
    // Normally, just render children
    return this.props.children
  }
}

function mapStateToProps(state) {
  return {
    state: state.errors
  }
}

function mapDispatchToProps(dispatch) {
  return {
    actions: bindActionCreators(ErrorActions, dispatch)
  }
}

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

You can test this by moving the error dispatch from getUsers into the success section of the action.

08 Components in Redux

branch: 08-components

In Redux, there are two different types of components mentioned in practice, Dumb components (stateless/presentational), and Smart components (stateful/container). Our container components are components which subscribe to the redux store and handle actions, where our dumb components are strictly for making use of state and actions. If you are familiar with React, you are probably quite familiar with this style of coding, where the container components are at the trunk of the tree pushing state and actions to the dumb components which are your leaves.

To build on this concept, and also stop us from continuing to cringe when looking at the App.js, let’s abstract the App.js file out to cater to this concept. We begin by creating our dumb components, so we can slowly replace the old App.js JSX without issue. I will be putting all dumb components in the folder App/components/visual.

Button.js

import React, { Fragment } from 'react'

export default ({text, ...buttonProps}) => (
  <Fragment>
    <button
      {...buttonProps}>
      {text}
    </button>
  </Fragment>
)

ProgressBar.js

 import React from 'react'

export default ({progress}) => (
  <div className="progress" role="progressbar">
    <div
      className="progress-meter"
      style={{ width: progress + '%' }}
      aria-valuemin="0"
      aria-valuetext={`${progress} percent done receiving users`}
      aria-valuemax="100">
    </div>
  </div> 
)

Text.js

import React from 'react'

export default ({text, ...textProps}) => (
  {text}
)

UserList.js

import React from 'react'

export default ({list}) => (
  <div className="user-list">
    {list.length > 0 ?
      <ul className="users light-scroll">
        {list.map((object, index) => (
          <li key={index} className="user-object">
            <p>{object.login}</p>
            <img src={object.avatar_url} /> 
          </li>
        ))}
      </ul>
    : ''}
  </div>
)

EDIT - 2020: Yes, this component is 2 levels too deep, this should be separated into two components, with the new component being a UserObject component.

Lastly we can update our App.js imports and render function:

 ...

import Text from './visual/Text'
import Button from './visual/Button'
import ProgressBar from './visual/ProgressBar'
import UserList from './visual/UserList'

...
render() {
  return (
    <div className="container">
      <Text
        text={this.props.state.counter.title}
        className="body"
      />
      <div className="body counter">
        <Text
          text={this.props.state.counter.total}
          className="counter-number"
        />
        <Button
          text="+"
          className="button btn-light"
          onClick={() => this.props.actions.counter.counterUp()}
        />
        <Button
          text="-"
          className="button btn-light"
          onClick={() => this.props.actions.counter.counterDown()}
        />
      </div>
      <div className="get-user-buttons">
        <Button
          text="Get Users"
          className="button"
          onClick={() => this.props.actions.users.userFetch()}
        />
      </div>
      {this.props.state.users.progress ?
        <ProgressBar progress={this.props.state.users.progress} />
      : ''}
      <div className="user-container">
        <UserList list={this.props.state.users.userList} />
      </div>
    </div>
  )
}

There isn’t a lot to this, just showing that every single dumb component is completely dependent on the state and actions from the container component. It will not only keep the amount of code used down, it will also make our app a lot easier to maintain. One huge benefit that comes with this, is that when an error occurs, the error will mention the error originating from one of these components. With this we can easily narrow the issue down to the code used within that component, including the state and actions.

You may also notice a few idioms used to make dumb components smaller. First is our anonymous function idiom, but you may also notice there isn’t a return object. Since these are dumb components, the need for the functional portion of the object which is wrapped in curly braces, and usually used for logic prior to returning the data. Since we do not make use of this section, we can bypass it by using parenthesis for returning JSX, which is exactly what we do.

09 Finishing Up

branch: 09-complete

I hope by this point you have noticed the power and potential of Redux, as well as why it’s so popular with larger apps, as well as how beneficial it is when it comes to centralizing your code.

Even though we are done with the app, there are a few specifics to mention. When dealing  with redux state in a development environment paired with the dev tools, if you get to a point where you have lots of actions being called over time you may notice slowdowns.  The reason is that the state is being accumulated, taking up more and more memory. Clicking the Commit button can remedy this issue, and no need to worry about this in production, it is only stored for development.

Along the lines of production apps, you may find that putting the DevTools container outside of the source directory to be a better answer. This is because in production you do not want to add the devtools, and babel can and will choke on it even when DevTools isn’t being loaded by the app itself.

Lastly, I highly encourage testing your application. While I absolutely love testing, adding testing to this guide would have made it much larger than it already is, though I would highly suggest reading over the Redux Testing Documentation.

I hope you enjoyed this guide, and if you would to check your version of the app against the finished app, you can find it here: Redux App

Happy coding!

 

No Comments

Add your comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.