The State Reducer Pattern

Control your state changes and allow "dumb" components fine-grained state management.

 


 

React has some pretty awesome patterns that allow for all sorts of composition and re-usability. State Reducer is one such pattern. This pattern allows us greater control of our state changes and the luxury of keeping our dumb components, dumb. By using per-component, custom reducers we can gate our setState calls and thus prevent unnecessary renders. This pattern isn't to be confused with Redux's reducers. Although, functionally, their APIs are similar this is a React-only pattern and is named with the more general CS "reducer" concept in mind. With that noted, let's check it out!

This article assumes you're familiar with React's Render Props pattern. If you're not quite cozy with it yet, I highly suggest this article.

Let's say we have a standard e-commerce site where we have a button that allows visitors to add items to their cart. This button is everywhere. It has various UI implementations, but generally it behaves identically with the exception of a few use cases here and there. The visitor sees something they want, they click it, and it adds that item to their shopping cart. Simple enough, right? Well, what if (for whatever reason) we wanted to limit the amount of a specific item they could place in their cart?

Let's pretend we have a limited supply of the item or -deviously- want to keep the supply low in an attempt to increase the demand (because... C.R.E.A.M.). Let's also assume we're lazy and we don't want to keep repeating button logic in multiple components. Assuming we're using Redux we could simply throw that stipulation into our reducer: we could check the number and type of them items in our store and prevent state changes there. But what if we're not using Redux? Further, what if we are but we want to keep this on the React side of things to keep our state clean? The State Reducer pattern allows us to cleanly manage our state's updates based on some criteria of our (or an implementors) choosing.

Using the Render Props pattern (again, see this post if you're unfamiliar), we'll create a wrapper for our buttons and pass the result of that logic to the stateless UI (dumb) components. We'll keep this example as simple as possible to keep the point in focus.

class ButtonWrapper extends React.Component {
  state = { added: 0 }

  controlledSetState (stateOrFunc) {
    this.setState(state => {
      /*  In this example we're only invoking the `controlledState` function in one place, so we
       *  can be sure `stateOrFunc` is a function. Nonetheless, we'll check here. In some use cases
       * it might be a normal piece of the state
       */
      const changedState = typeof stateOrFunc === 'function' ? stateOrFunc(state) : stateOrFunc

      /* `stateReducer` is guaranteed to return an empty object in this example,
       * but it could be possible in other use cases that the return value is null.
       * This will make sure we at least have a {} to return appropriately below.
       */
      const reducedState = this.props.stateReducer(state, changedState) || {}

      /* We'll return the `reducedState` if something interesting happens
       * i.e. we stay below our limit of 8. In other instances ( >= 8) we'll return `null`
       * to prevent excessive re-renders.
       */
      return Object.keys(reducedState).length > 0
      ? reducedState
      : null
    })   
  }

  addToCart = () => {
    // Just being explicit with this assignment for readability
    const fn = ({ added }) => ({ added: added + 1 })
    this.controlledSetState(fn)
  }

  getWrapperState () {
    return {
      addToCart: this.addToCart,
      added: this.state.added,
    }   
  }

  render () {
    return this.props.children(this.getWrapperState())   
  }
}

I think this example speaks for itself, but basically the only time we setState is if there was a result from the stateReducer (i.e. a non-empty object of state changes were returned). Otherwise we return null from setState which does nothing to the overall application (no renders). But how might this actually be used? Let's do that too.

class CartButton extends React.Component {

  /*
   *  This is the only "intelligent" thing our dumb component
   *  does: define its reducer. This is then passed on to
   *  the ButtonWrapper as a prop.
   */ 
  addToCartStateReducer = (currentState, updatedState) => {
    if (currentState.added >= 8) {
      return { }   
    }   
    return updatedState
  }

  render () {
    return (
      <ButtonWrapper
        stateReducer={this.addToCartStateReducer}
      > 
        {buttonLogic => (
          <div>
            <button onClick={buttonLogic.addToCart}>Add Item</button>
          </div>
        )}
      </ButtonLogic>
    )     
  }
}

Our CartButton defines its own reducer. In this implementation we've chosen to limit the number of items a person can add to their cart to eight. Aside from the addToCartStateReducer method, this is a very simple component. It's main job is to display a button and our ButtonWrapper is responsible for everything else. Pretty clean, right?

The State Reducer pattern clearly has some utility, but it does break encapsulation. We're checking ButtonWrappers state in CartButton. Despite this it does allow for cleaner and more reusable state management while maintaing the ability for separation of concerns.

comment

Comments