Redux manufacturing: managing state and inventory

This is part three, “Managing state and inventory”, of 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 post in this series, we built out the initial components that modeled creating widgets in our warehouse. We defined a top-level collection of objects, known as state, that collected raw materials, produced widgets, and any error messages or “bad” widgets (determined by the warehouse’s QA department). This state was stored in the App component: in doing so, we were able to pass various parts of state (materials, widgets) into the components as props that interfaced with those objects.

We also defined an addWidget action, which was passed into the Manufacturing component. This action also was passed as a prop to the component, and in doing so, the actual “logic” around what creating a component entails is not strictly tied to the view layer.

In this post, we’ll finish implementing the basic pieces of our warehouse–we’ll create orders, which contain a set number of required widgets, and allow packaging and shipping these orders. Once we’ve built this functionality, we’ll introduce Redux, and begin to understand how it manages state, and modifying data via actions.

Generating orders

From the perspective of a customer, the order is essentially the only view into our warehouse. This is an oversimplification, as orders themselves are very interesting. An order is self-contained when created: it contains a timestamp, indicating when it was created (this is to have a concept of “uniqueness” in our HTML code–see React’s keys documentation for more info), as well as the number of widgets required to fill the order.

Filling the order is more interesting. Not only is the order itself “destroyed” in the process of being filled, as it changes into a packaged order; this process of filling the order also removes widgets from the widget part of our state. Because of this, the flow of an order throughout the app touches almost every part of state.

The Orders component itself receives two props: an action, generateOrder, which creates a new order and appends it to the orders array in state, and the orders array itself, to display each order and the number of required widgets to fill it:

// App.js

import Orders from './Orders'

class App extends Component {
  constructor() {
    super()

    this.state = {
      // ...
      orders: []
      // ...
    }

    this.generateOrder = this.generateOrder.bind(this)
  }

  generateOrder() {
    const newOrder = {
      created: Date.now,
      widgets: Math.floor(Math.random() * 10) + 1
    }
    const newOrders = [].concat(this.state.orders, newOrder)
    const newState = Object.assign({}, this.state, {
      orders: newOrders
    })
    this.setState(newState)
  }

  render() {
    const { error, failed, materials, orders, widgets } = this.state
    return (
      <div>
        { /* ... */ }
        <Orders 
          generateOrder={this.generateOrder} 
          orders={orders} />
        { /* ... */ }
      </div>
    );
  }
}

// Orders.js

class Orders extends Component {
  render() {
    const { generateOrder, orders } = this.props
    return (
      <div>
        <h2>Orders</h2>
        <h3>{orders.length} orders</h3>
        <ul>
          {orders.map(order => <li key={order.created}>Order for {order.widgets} widgets</li>)}
        </ul>
        <button onClick={generateOrder}>Generate order</button>
      </div>
    )
  }
}

(commit 0c6741)

Packaging orders

The Packaging component receives three props, but this doesn’t mean that it’s especially complicated. The packageOrder, much like our previous actions, removes the most recent order from the orders array and, given enough available widgets, packages them to increment the packaged count. It also receives packaged and orders, in order to display a button for packaging an order. If no orders are available, the button is hidden from the interface.

At this point, you may find that it becomes difficult in the UI to actually build and package widgets, because the number of raw materials remains static. The Management component provides a simple button to increase the number of available raw materials. Of course, this component could be expanded with a number of other functions, but I found that this was a necessary addition to actually test the end-to-end flow of widget delivery in the application. Code for the addition of both components follows:


// App.js import Management from './Management' import Packaging from './Packaging' class App extends Component { constructor() { super() this.state = { // ... packaged: 0 // ... } this.orderMaterials = this.orderMaterials.bind(this) this.packageOrder = this.packageOrder.bind(this) } // ... orderMaterials() { const { materials } = this.state const { dowel, screw, wheel } = materials this.setState(Object.assign({}, this.state, { materials: { dowel: { count: dowel.count + 10 }, screw: { count: screw.count + 10 }, wheel: { count: wheel.count + 10 } } })) } packageOrder() { const { orders, packaged, widgets } = this.state const newOrders = [].concat(orders) const order = newOrders.pop() if (order.widgets <= widgets.length) { this.setState({ orders: newOrders, packaged: packaged + 1, widgets: widgets - order.widgets }) } else { this.presentError("Not enough widgets to fill order!") } } // ... render() { const { packaged } = this.state return ( <div> { /* ... */ } <Packaging orders={orders} packaged={packaged} packageOrder={this.packageOrder} /> <Management orderMaterials={this.orderMaterials} /> </div> ); } } // Management.js class Management extends Component { render() { return ( <div> <h2>Management</h2> <button onClick={this.props.orderMaterials}>Order raw materials</button> </div> ); } } // Packaging.js class Packaging extends Component { render() { const { orders, packageOrder, packaged } = this.props return ( <div> <h2>Packaging</h2> <h4>{packaged} orders packaged</h4> {orders.length ? <button onClick={packageOrder}>Package order</button> : <h4>No orders to package</h4>} </div> ); } }

(commit 0c6741)

Shipping orders

The Shipping component is incredibly simple, and somewhat of a let-down for our finale. The shipOrder prop delegates shipping a package to our shipOrder function, which simply increments the number of shipped orders in state, while decrementing the number of packaged orders. Again, we pass in packaged to hide the “Ship order” button if there are no packages to ship, and shipped to count how many packages have been shipped.

// App.js

import Shipping from './Shipping'

class App extends Component {
  constructor() {
    super()

    this.state = {
      // ...
      shipped: 0,
      // ...
    }

    // ...
    this.shipOrder = this.shipOrder.bind(this)
  }

  // ...

  shipOrder() {
    const { packaged, shipped } = this.state
    this.setState({
      error: null,
      packaged: packaged - 1,
      shipped: shipped + 1
    })
  }

  render() {
    const { shipped } = this.state
    return (
      <div>
        { /* ... */ }
        <Shipping packaged={packaged} shipped={shipped} shipOrder={this.shipOrder} />
        { /* ... */ }
      </div>
    );
  }
}

// Shipping.js

class Shipping extends Component {
  render() {
    const { packaged, shipped, shipOrder } = this.props
    return (
      <div>
        <h2>Shipping</h2>
        <h4>{shipped} orders shipped</h4>
        {packaged ? <button onClick={shipOrder}>Ship order</button> : <h4>No orders to ship</h4>}
      </div>
    );
  }
}

(commit cf6299)

Introducing Redux: Data flow

Throughout this series, I’ve been intentional in introducing our data and actions in a single place as much as possible: the App component. Because of this, we can easily intuit how the data flows, and how actions interface with the application state. Parts of our application state, like widgets or error, are pushed into components, but they’re never modified in the component themselves. Instead, we pass a function, like addWidget, as a prop to be called by the component. In doing so, we update state in App using .setState, and pass the new version of state into the respective components.

This may have seemed verbose, but it was for a reason–this is almost identical to how we will handle data with Redux.

A great deal of confusion can come out of trying to introduce Redux to an existing project. The reason for this isn’t because writing Redux code is particularly hard; it’s because we often don’t understand how data flows in our application. Redux makes this a non-issue, because you have no choice.

Data is always unidirectional in Redux. The data arrives at the top level component, and is passed down to various components as needed, often as subsets of the original state–for instance, passing only error to the Error component. These components, where state is essentially “injected”, are called containers.

There are four different things we must know to implement Redux in our application: actions, reducers, the store, and containers. We’ll cover each of them in brief here, but in the next post, we’ll rewrite our application to use them directly, and thus have a complete React and Redux application.

Actions

Actions provide an interface into modifying the state of our application. The functions we’ve written and passed as props into various components are similar in concept to Redux actions–they act as signals to update state. The difference is that a Redux action does not actually do the state updating itself. Instead, it passes along an identifier to a reducer to let it know that state needs to be updated. Below is an example of what an addWidget action might look like in Redux:

// actions.js

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

Reducers

A reducer handles updating state. A reduce operation takes an input and a function, and applies them to produce a single output. In the same way, a Redux reducer receives state as input, and updates it based on the requested action–for instance, ADD_WIDGET:

// reducers.js

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

const initialState = {
  error: null,
  failed: 0,
  orders: [],
  materials: {
    dowel: { count: 2 },
    screw: { count: 8 },
    wheel: { count: 3 }
  },
  packaged: 0,
  shipped: 0,
  widgets: []
}

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
}

export default function appState(state = initialState, action) {
  switch (action.type) {
    case ADD_WIDGET:
      return addWidget(state, action.date)
    // ...
    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.

It’s important to note the introduction of constantsADD_WIDGET, for instance, is used in both our action example and in our reducer. By defining a single source of truth for our action names, we can ensure that the actions and reducers properly handle the same actions. Under the hood, constants are simply strings:

export const ADD_WIDGET = 'ADD_WIDGET'

The usage of constants also makes debugging straightforward: with a tool like redux-logger, you can follow your application as it moves through actions by looking for ADD_WIDGET. Because it’s unlikely this string will be used in any place besides your actions and reducers, it brings some extra clarity to your application.

The initialState variable, identical to our initial state in the App component, is provided as the default argument to appState. The appState function receives a state object and an action, and depending on what the action is, returns a variant of the state argument, or leaves it unchanged.

The addWidget function pulls out the relevant logic into a separate method, keeping the appState function relatively clean. It’s important to know that your state object should be treated as immutable: instead of modifying the state argument passed in to addWidget, you should create a new object and assign the relevant parts of the existing state, as well as overwrite the new/updated state from newState. Redux’s documentation contains an incredibly detailed section on immutability, but it’s also succinctly described in one of Redux’s core tenets: never mutate state.

By providing a pure reducer function where state comes in and new state comes out, we’re architecting a data flow in which it’s always clear how and when data changes. Changes to state only happen in the reducer; changes to state are always propagated to components. It’s a great mental model that makes debugging and reasoning about your code easier.

Containers and stores

Actions and reducers are basically a collection of simple JavaScript objects and functions. Without connecting them to our application, they’re fairly useless. This is where the store and containers come in. A Redux 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.

While the implementation of “connecting” a container to the Redux store isn’t particularly difficult, it’s recommended that you make use of react-redux, which exposes a simple API for creating containers. To begin, we’ll wrap our App component in a higher-order component, or HOC. An HOC receives a component as input, and returns a new component as output. react-redux provides the Provider component, which accepts a store prop and makes it available to its wrapped component; in our case, App:

// index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import appState from './reducers';
import App from './App';
import './index.css';

const store = createStore(appState);

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

Our App component now has access to our Redux store. We can use the connect function from react-redux to gain access to our application state and actions:

// App.js
// ...
import { connect } from 'react-redux'
import { addWidget } from './actions'

class App extends Component {
  render() {
    const {
      addWidget,
      widgets
    } = this.props

    return (
      // ...
    );
  }
}

const mapStateToProps = (state) => state
const mapActionCreators = { addWidget }

export default connect(
  mapStateToProps,
  mapActionCreators
)(App);

(commit b13516)

(Looking for the rest of the action and reducer code? Check out the next post in this series, where we fully transition our application state and actions into Redux.)

The connect function requires two arguments–a mapStateToProps function, which takes application state as an input and prepares it to be injected as props to our component, and a mapActionCreators object, which prepares our actions to be callable as props from our component.

In the render function, the widgets portion of state is now available inside of this.props, whereas previously we retrieved it from this.state. In the same way, our addWidget action, previously defined on the component itself, is now just a prop. Either can be passed to child components as simple props, and calling addWidget would fire the action on our Redux store, and potentially update widgets via the reducer.

Conclusion

In implementing Redux in our application, we’ve removed almost all the logic of operating the warehouse from the components itself. This makes sense if you recall React’s design as “the view layer”: it shouldn’t really be concerned with how the warehouse operates. It should only focus on providing the interface to operating the warehouse.

In the final part of this series, we’ll fully transition our warehouse application into a Redux and React application. We’ll explore more complex reducer logic, and migrate our application actions using both basic actions, and “thunked” actions.

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.

Read the next post in this series, “Building cohesion”.

Leave a Reply

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