Redux manufacturing: building cohesion

This is the final post, “Building cohesion”, in Redux manufacturing, an introductory series to the JavaScript library Redux. New readers are encouraged to start at the beginning of this series, “Learning by analogy”, here.

In the previous part of this series, we introduced Redux to our application, and began to explore actions, reducers, the store, and containers. To recap:

  • Actions provide an interface into modifying the state of our application.
  • A reducer handles updating state.
  • The store is our interface to actions and reducers: it provides access to the application state, as well as allowing updating the store via dispatching actions.
  • Containers are React components that interface with our Redux store–a component is “wrapped” with a connect function that ties it explicitly to the Redux store.

In the previous post, we implemented a single action in Redux: addWidget. This action was passed as a prop to the App component, and could be called in the interface. When the action was called, a new widget was added via the addWidget function in the reducer. This function returned a new version of application state with an incremented widgets count, but with the necessary raw materials removed from our materials object.

A note at this point: the work in this post is contained in a single commit, located here. A summarized collection of code is available below, with the complete source available at bytesizedxyz/thewidgetcompany on GitLab.

The “thunked” action

The process of creating a widget has two different potential failures:

  • Not enough raw materials to create a widget
  • The widget fails the QA check

The QA check, as you may recall, is a simple random number check–this happens as a function in the reducer. The first error, where not enough materials are present, is more complex than it first appears.

Consider the basic addWidget action that we made in the previous post:

// actions.js

export function addWidget() {
  return {
    date: Date.now(),
    type: ADD_WIDGET
  } 
}

As it stands, this action doesn’t contain any logic. It returns a static object that always contains the ADD_WIDGET type, and a generated date. If we were to check in this action that we have the raw materials for a widget, we would need additional information: specifically, our current application state.

At this point, our action needs to be a function, not just a plain object. To implement this, we’ll use redux-thunk, a simple library written by Redux’s author. redux-thunk allows you to create a thunk, which is an action that allows you to delay its execution. Let’s add redux-thunk to our application, first by adding the necessary middleware to our store:

// index.js
import { applyMiddleware, createStore } from 'redux';
import thunk from 'redux-thunk';
import appState from './reducers';

const store = createStore(
  appState,
  applyMiddleware(thunk)
);

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

With the redux-thunk middleware in our store, we can change our addWidget action to check for the necessary materials–if they exist, proceed to create a widget; if not, call the new presentError action, which will dispatch an error to our store. We’ll rename the addWidget action to attemptWidgetCreation, to indicate that this new action entails a bit of logic about our application:

// constants.js
export const ADD_WIDGET = 'ADD_WIDGET'
export const PRESENT_ERROR = 'PRESENT_ERROR'

export const DOWELS_NEEDED = 1
export const SCREWS_NEEDED = 2
export const WHEELS_NEEDED = 2

// actions.js
import {
  ADD_WIDGET,
  PRESENT_ERROR,
  DOWELS_NEEDED,
  SCREWS_NEEDED,
  WHEELS_NEEDED
} from './constants'

export function attemptWidgetCreation() {
  return (dispatch, getState) => {
    const { materials } = getState()
    const { dowel, screw, wheel } = materials

    if (dowel.count >= DOWELS_NEEDED && screw.count >= SCREWS_NEEDED && wheel.count >= WHEELS_NEEDED) {
      dispatch(addWidget())
    } else {
      dispatch(presentError("Not enough materials to create a widget"))
    }
  }
}

export function addWidget() {
  return {
    date: Date.now(),
    type: ADD_WIDGET
  }
}

export function presentError(message) {
  return {
    type: PRESENT_ERROR,
    message
  }
}

The new function returned in attemptWidgetCreation allows us to defer the execution of the action. We retrieve the state from the getState function, and ensure that the number of dowels, screws, and wheels is enough to create a widget. Once we’ve done this check, we can either dispatch the original addWidget action, or presentError, which accepts a message to be passed into application state.

In our reducer, the code to handle the ADD_WIDGET action remains the same. More code is added to handle PRESENT_ERROR, which will set error in our application state to the provided message:

// reducers.js
const addWidget = (state, date) => {
  const newMaterials = state.materials
  newMaterials.dowel.count -= DOWELS_NEEDED
  newMaterials.screw.count -= SCREWS_NEEDED
  newMaterials.wheel.count -= WHEELS_NEEDED

  let newState = { materials: newMaterials }

  if (qaCheck()) {
    const newWidget = { created: date }
    const newWidgetsInventory = [].concat(state.widgets, newWidget)
    newState.error = null
    newState.widgets = newWidgetsInventory
  } else {
    newState.failed = state.failed += 1
    newState.error = "A widget failed QA!"
  }

  return Object.assign({}, state, newState)
}

const qaCheck = () => {
  const check = Math.floor(Math.random() * 10)
  return check < 7
}

const presentError = (state, message) => {
  return Object.assign({}, state, {
    error: message
  })
}

export default function appState(state = initialState, action) {
  switch (action.type) {
    case ADD_WIDGET:
      return addWidget(state, action.date)
    case PRESENT_ERROR:
      return presentError(state, action.message)
    default:
      return state
  }
}

Update: acemarke on Reddit points out that generating a Date in a reducer makes it impure–this goes against standard Redux practices. The relevant code in both our action and reducer function have been updated, with a new constructed Date instance being generated in the action and passed to the reducer. See the relevant commit here.

This “thunked” action should be used when you need to make a determination around what action should be performed based on state. It can also be used for request-based actions–if you needed to make a request and then update the store based on the response, you could do something like:

export function getThing() {
  return dispatch => {
    fetch(aURL).then(resp => dispatch(updateThing(resp)))
  }
}

Of course, not all actions require being wrapped in a thunk. Our addWidget and presentError actions are immediate–when called, they instantly return an object with a distinguishing action type. In the case of presentError, we see that actions can accept arbitrary arguments, which can be passed along to the store or used for determining logic in the action itself.

All the actions

Packaging orders

Thunked actions are interesting, but most of the actions that take place in our application are straightforward enough that we’ll only create one more instance of this action type. When an order is packaged, the current implementation checks the available number of widgets and compares it to the current order: if there are enough widgets, we can package a new order; if not, an error message should be presented. The action replacement for packageOrder, checkOrderForPackaging, retrieves the state and confirms that enough widgets exist before continuing to package the order:

// constants.js
export const PACKAGE_ORDER = 'PACKAGE_ORDER'

// actions.js
export function checkOrderForPackaging() {
  return (dispatch, getState) => {
    const { orders, widgets } = getState()
    const order = orders[0]
    if (order.widgets <= widgets.length) {
      dispatch(packageOrder(order))
    } else {
      dispatch(presentError("Not enough widgets to fill order!"))
    }
  }
}

export function packageOrder(order) {
  return {
    type: PACKAGE_ORDER,
    order
  }
}

// reducers.js
const packageOrder = (state, order) => {
  const newWidgets = state.widgets

  for (var i=0; i < order.widgets; i++) {
    newWidgets.shift()
  }

  let newOrders = [].concat(state.orders)
  newOrders = newOrders.filter((obj, index) => index != state.orders.indexOf(obj))

  return Object.assign({}, state, {
    error: null,
    orders: newOrders,
    packaged: state.packaged + 1,
    widgets: newWidgets
  })
}

export default function appState(state = initialState, action) {
  switch (action.type) {
        // ...
    case PACKAGE_ORDER:
      return packageOrder(state, action.order)
    default:
      return state
  }
}

(Note that there are two bugs with the primary commit for this post. The fixed implementation is shown above, but two commits are available in GitLab to fix the primary commit. The first fixes a bug with the checkOrderForPackaging implementation, and is available here. The second fixes the reducer behavior of packaging an order, and is available here.)

In implementing this logic in the action and reducer, our Packaging component now simply accepts the checkOrderForPackaging action from the App component–all the determination around when and how to package an order is removed from our view layer:

// App.js
class App extends Component {
  render() {
    const { orders, checkOrderForPackaging, packaged } = this.props
    return (
      <div>
        { /* ... */ }
        <Packaging orders={orders} packaged={packaged} packageOrder={checkOrderForPackaging} />
        { /* ... */ }
      </div>
    )
  }
}

Generating an order

The remainder of our actions are simple, non-thunked implementations. Because of this, we’ll breeze through them since they follow a similar pattern. Generating an order is the first of these:

// constants
export const GENERATE_ORDER = 'GENERATE_ORDER'

// actions.js
export function generateOrder() {
  return {
    date: Date.now(),
    type: GENERATE_ORDER
  }
}

// reducers.js
const generateOrder = (state, date) => {
  const newOrder = {
    created: date,
    widgets: Math.floor(Math.random() * 10) + 1
  }
  const newOrders = [].concat(state.orders, newOrder)
  return Object.assign({}, state, {
    orders: newOrders
  })
}

export default function appState(state = initialState, action) {
  switch (action.type) {
    // ...
    case GENERATE_ORDER:
      return generateOrder(state, action.date)
    // ...
  }
}

None of this code is particularly new–the bulk of the logic, the generateOrder function in reducers.js, is simply ported over from our original implementation in the App component. Instead of referring to this.state, we can look at the current state object passed into the function, which is simply state. Most importantly, we instantiate a new state object, which is compiled from the old state and an object that contains the updated newOrders. Again, this concept of immutable state and not mutating the object that already exists fits neatly into the tenets of designing an application with Redux.

Ordering materials

Ordering materials is a similar flow to generating an order:

// constants
export const ORDER_MATERIALS = 'ORDER_MATERIALS'

// actions.js
export function orderMaterials() {
  return {
    type: ORDER_MATERIALS
  }
}

// reducers.js
const orderMaterials = (state) => {
  const { materials } = state
  const { dowel, screw, wheel } = materials
  return Object.assign({}, state, {
    error: null,
    materials: {
      dowel: { count: dowel.count + 10 },
      screw: { count: screw.count + 10 },
      wheel: { count: wheel.count + 10 }
    }
  })
}

export default function appState(state = initialState, action) {
  switch (action.type) {
    // ...
    case ORDER_MATERIALS:
      return orderMaterials(state)
    // ...
  }
}

Shipping an order

Finally, shipping an order is likely the easiest implementation so far. Note the lack of any variable definitions in the reducer function: it just increments based on what is already in state:

// constants
export const SHIP_ORDER = 'SHIP_ORDER'

// actions.js
export function shipOrder() {
  return {
    type: SHIP_ORDER
  }
}

// reducers.js
const shipOrder = (state) => {
  return Object.assign({}, state, {
    error: null,
    packaged: state.packaged - 1,
    shipped: state.shipped + 1
  })
}

export default function appState(state = initialState, action) {
  switch (action.type) {
    // ...
    case SHIP_ORDER:
      return shipOrder(state)
    // ...
  }
}

(Note that the primary commit for this post is accidentally missing the shipOrder action displayed above. A fix for this is available here.)

View the final version of The Widget Company application at bytesizedxyz/thewidgetcompany on GitLab.

Conclusion

We began this series exploring how the warehouse suffered from a number of confusing design details. Like many software applications, it was unclear how data moved between components, and the responsibility of a component determining how and when it should perform an action was quite vague.

Implementing the original App component with a top-level state was a bit of a trick: in doing so, we already gave ourselves a head-start on implementing the same behavior in Redux. With the addition of Redux into our project, our data flow became unidirectional. Data flows from the store, down through the container, into the components. Actions flow much in the same way, and when one is called, the new version of state is passed down just as it had already done once before.

Understanding unidirectional data flow isn’t just key to understanding React; it’s crucial in designing high-functioning React applications. Given a new feature or piece of functionality, it’s clear where it should go. Given a request to modify existing behavior, we can always be sure where that logic is defined. This application has gone from relative instability to a straightforward, well-separated collection of React components, and vanilla JavaScript code.

Where to proceed from here? Given the availability of redux-thunk in our project, we’re in a position to implement any number of complex behaviors in our application. If we wanted to move our order generation to a different application altogether, it’s a simple thunked request away. Making the QA department more complex by introducing an explicit check based on a network request, or an attribute of the widget, is feasible–we know where the current code lives, and how and where our updated version could live.

At the beginning of this series, I explained why analogy is an effective tool for learning:

Learning by analogy has proven to be an incredibly effective method I’ve used to learn a variety of new things. The reason for this is simple: it is tangibly easier to connect pieces you already have over discovering and remembering entirely new pieces. After all, when building anything, it’s necessary to first take stock of what you have, before determining what new things that you need.

It’s difficult to explain why unidirectional data flow matters. It’s hard to grasp at why separating actions and reducers from your components is useful. By implementing a real application, and by allowing each component in the application to model a (somewhat real) concern in our warehouse analogy, it became easier to see the project evolve over time. Our initial implementation wasn’t bad, but the advantages of the Redux approach should be clear. Overall, we’ve built an application that I would be happy to continue to work on in the future, and one that new developers would be quite comfortable getting up and running with.

Found this series useful? Check out some of the other posts on our blog, like the free React.js series “A year with React.js”.

Bytesized offers technical training for companies. If you or your company want to take a deeper dive into React.js or Redux, including hands-on workshops, lectures, and take-home exercises, contact us to schedule training. We’re excited to work with you.

Leave a Reply

Your email address will not be published. Required fields are marked *