DEV Community

Cover image for Managing state with useReducer
Nick DAmbrosio
Nick DAmbrosio

Posted on • Edited on

Managing state with useReducer

Last time we looked into React Hooks and why they've been introduced, then we refactored a react component built as a class into a function component using hooks, allowing us to share logic between components. Now we're going to look at managing complex state with useState and useReducer.

See the code in this blog in action at

Counter with useState and useReducer

React useState hook


useState is a basic hook that allows us to have local state in a functional component.

We create one by declaring a state variable: counter followed by a function to update it: setCounter.

Initialize the count to zero by passing it 0 as the only argument to useState().

Lastly, because we want to update our state by adding 1 to the previous state(initialized at 0), we need to provide a function of the previous state as an argument to update it.

If you're setting a value that does not rely on the value of the old state, then you can use a value as the argument, otherwise, use a function.

function Counter() {
  const [counter, setCounter] = useState(0)
  const increment = () => setCounter(counter + 1)

  return (
    <div>
      <div>{counter}</div>
      <button type="button" onClick={increment}>
        +
      </button>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Notice counter (value) and setCounter are being returned as an array from useState(). The order here is important, The setter function will control the state that is managed by the hook.

πŸ€”πŸ€” What happens when we want another piece of state?

function Counter() {
  const [count, setCount] = useState(0)
  const [disabled, setDisabled] = useState(false)

  const increment = () => setCount(count + 1)
  const toggleDisabled = () => setDisabled(!disabled)

  return (
    <div>
      <div>{count}</div>
      <button type="button" disabled={disabled} onClick={increment}>
        +
      </button>
      <button type="button" onClick={toggleDisabled}>
        {disabled ? "enable" : "disable"}
      </button>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

We can do it as many times as we want! Each useState is unique and can function independently of each other.

However, after writing your third or fourth useState in the same component, you start to feel like that there's got to be something better. If your component needed the ability to undo, you'd have to keep track of the previous state ontop of the current state for every operation.

Let's add some extra capabilities to our counter by adding the ability to decrement, reset, and undo.

function Counter({ initialValue = 0 }) {
  const [previousCount, setPreviousCount] = useState(null)
  const [count, setCount] = useState(0)

  const increment = () => {
    setPreviousCount(count)
    setCount(count + 1)
  }

  const decrement = () => {
    setPreviousCount(count)
    setCount(count - 1)
  }

  const reset = () => {
    setPreviousCount(null)
    setCount(initialValue)
  }

  const undo = () => {
    setCount(previousCount)
    setPreviousCount(null)
  }

  const toggleDisabled = () => setDisabled(!disabled)

  return (
    <div>
      <div>{count}</div>
      <button type="button" onClick={increment}>
        +
      </button>
      <button type="button" onClick={decrement}>
        -
      </button>
      <button type="button" onClick={reset}>
        Reset
      </button>
      <button type="button" onClick={undo} disabled={!prevValue}>
        Undo
      </button>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Hrmm. We did it, but that's a lot of slightly different, but mostly repetitive, code.





React useReducer hook πŸ”₯πŸ”₯πŸ”₯


What is useReducer ? It's a hook based off of Javascript's very popular reduce function. Definitely a topic that will require it's own blog post eventually, but until then, here are a few outstanding posts by other people on it

Sarah Drasner: The Almighty Reducer

Kent C Dodds: Array reduce vs chaining vs for loop

An important takeaway of the reducer is it will always return one value, By reducing the previous value down to a single representative one. Reducers are incredibly powerful and it's hard to get far on the internet without seeing them referenced.

The reducer function is a function that receives a state and an action, then reduces to a new state.

(state, action) => newState

The useReducer hook is an alternative to useState, meaning you can use the two interchangeably. However, useReducer is preferred over useState when you have complex state logic, especially when the next state is dependant on the previous state.

useReducer takes two arguments: First being the reducer() method itself and the second is a dispatch method that calls the reduer() method.

The useReducer hook looks like this. It's a function that receives a state and an acation, and reduces it to a new value, in our case: state.

const [state, dispatch] = useReducer(reducer, initialState)
Enter fullscreen mode Exit fullscreen mode

Let's rebuild the previous Counter component using useReducer instead of useState

const CounterReducer = ({ initialState = 0 }) => {
  const reducer = (state, action) => {
    switch (action.type) {
      case "increment":
        return {
          count: state.count + 1,
          prevValue: state.count,
        }
      case "decrement":
        return {
          count: state.count - 1,
          prevValue: state.count,
        }
      case "reset":
        return {
          count: initialState,
          prevValue: null,
        }
      case "undo":
        return {
          count: state.prevValue,
          prevValue: null,
        }
      default:
        return state
    }
  }

  const [state, dispatch] = useReducer(reducer, {
    count: initialState,
    prevValue: null,
  })

  return (
    <div>
      <h1>useReducer Counter: {state.count}</h1>
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
      <button onClick={() => dispatch({ type: "reset" })}>Reset</button>
      <button
        onClick={() => dispatch({ type: "undo" })}
        disabled={!state.prevValue}
      >
        Undo
      </button>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Nice! And it looks pretty similar to the architecture used in popular state management library, Redux.

Instead of using a bunch of useState updater functions, useReducer centralizes the state logic of the component into a single reducer function.

Our lord and savior Dan Abramov summed up in a tweet of when to use useState or useReducer when managing component state:

When to use useReducer

Should I use multiple useState or useReducer?

For independent things (isHovering and textInput), multiple useState.

For things that change together (isFetching and fetchedItems), or if their next state depends on previous (todos), I prefer useReducer.https://t.co/wSyKITMRpZ

β€” Dan Abramov (@dan_abramov) January 10, 2019

For me, I use useState until it feels like the components stateful logic is overly complex. Forms and CRUD applications are a great example of when you'd benefit from useReducer over useState. We'll build a todo-list using useReducer next time.

Recap

  • useReducer > useState when handling complex state logic that involves multiple sub values, or when the next state is reliant on the previous state

  • When called, useReducer will return an array of two items: Current State and a Dispatch method

  • useReducer accepts three arguments:

    • reducer function
    • initial state
    • third, optional init function for lazy initialization of state
  • Reducers are responsible for handliong transitions from one state to the next. They take in current state and an action and return a new state

  • Actions are unique events that happen in your app

  • Actions are dispatched to our reducer when specific events take place.

Hot Damn that was a long post. Thanks for reading!

this was originally posted April 22, 2020 at Hooks-and-Hamburgers.com

Top comments (0)