Skip to content

How to use useReducer hook

daisho edited this page Sep 26, 2021 · 6 revisions

I've learnt this in frontendmasters by my favorite instructor Brian Holt.

useReducer? This sounds like Redux, right? And that's because it actually is very similar to Redux. So let's take a look at how we end up implementing that here.

In specific cases, we will pull on useReducer instead of useState. They fundamentally achieve the same things in relatively different ways.

For this demonstration, we write all functions in one file.

import { useReducer } from 'react'

// fancy logic to make sure the number is between 0 and 255
const limitRGB = num => (num < 0 ? 0 : num > 255 ? 255 : num)
const step = 50

const reducer = (state, action) => {
  switch (action.type) {
    case 'INCREMENT_R':
      return Object.assign({}, state, { r: limitRGB(state.r + step) })
    case 'DECREMENT_R':
      return Object.assign({}, state, { r: limitRGB(state.r - step) })
    case 'INCREMENT_G':
      return Object.assign({}, state, { g: limitRGB(state.g + step) })
    case 'DECREMENT_G':
      return Object.assign({}, state, { g: limitRGB(state.g - step) })
    case 'INCREMENT_B':
      return Object.assign({}, state, { b: limitRGB(state.b + step) })
    case 'DECREMENT_B':
      return Object.assign({}, state, { b: limitRGB(state.b - step) })
    default:
      return state
  }
}

const ReducerComponent = () => {
  const [{ r, g, b }, dispatch] = useReducer(reducer, { r: 0, g: 0, b: 0 })

  return (
    <div>
      <h1 style={{ color: `rgb(${r}, ${g}, ${b})` }}>useReducer Example</h1>
      <div>
        <span>R</span>
        <button onClick={() => dispatch({ type: 'INCREMENT_R' })}>+</button>
        <button onClick={() => dispatch({ type: 'DECREMENT_R' })}>-</button>
      </div>
      <div>
        <span>G</span>
        <button onClick={() => dispatch({ type: 'INCREMENT_G' })}>+</button>
        <button onClick={() => dispatch({ type: 'DECREMENT_G' })}>-</button>
      </div>
      <div>
        <span>B</span>
        <button onClick={() => dispatch({ type: 'INCREMENT_B' })}>+</button>
        <button onClick={() => dispatch({ type: 'DECREMENT_B' })}>-</button>
      </div>
    </div>
  )
}

export default ReducerComponent

Why would we choose this over useState?

This looks a little bit more complicated, perhaps it is. We could've totally done this with useState.

const [r, setR] = useState(50)
const [g, setG] = useState(50)
const [b, setB] = useState(50)

And then, instead of dispatching an action here, we just would've called setR or setG. It's totally valid, and it would work exactly the same way.

When people just like this methodology of using reducers, because they've used Redux for so long they wanna keep using that. That's one reason that people use it.

But the reason that I think it's useful is we have a lot of state that's modified in relatively uniform fashions. And we can see all of them at a glance here. This is all of the way that our state changes in a single glance.

The source of almost every bug is just state changing in the wrong way, right? And in this particular view, we can see at a glance all of the changes that this specific component can go through. And we can comprehend it as a whole. That's valuable. That makes it very readable.

The other thing is that we can pull this reducer out into a unit test. And we can unit test that if we give it this state and action, it comes back with the state that we expect. This means that we get to change or test how our state will change, which is very valuable. So then we can write unit tests around that and prevent bugs from state changing incorrectly, which is the source of most bugs. And if you can test the source of most bugs, we can have more confidence in our application.


P.S. This line can be replaced

return Object.assign({}, state, { r: limitRGB(state.r + step) })

with ES6 syntax.

return { ...state, ...{ r: limitRGB(state.r + step) } }

Important thing here is reducer should NOT mutate the state. It has to return a new state/object.

Clone this wiki locally