< - back to see all blog posts

When to use the useReducer() hook?

July 08, 2019

You might be familiar with one of the React hooks called useReducer(). You know WHAT useReducer() is and how it works but maybe the context is missing – to know WHEN to use it.

WHEN?

when-to-use-useReducer-checks-watch

Here’s when:

  • always use useReducer()…

    • When two or more states change together
    • When your state relies on another state or previous state.
  • sometimes useReducer()…

    • When you want to beautify / organize your code
    • When your states get too complicated
    • When you want to do less “plumbing”
    • When you’d like an easier way to make tests
    • When you don’t know if you should use useReducer()
  • never use useReducer()…

    • When you only have one individual state(s) in a component

Using useReducer() is never necessary. You can always use the useState() hook but in some cases it can make your life much better and your more code pro if you know when to use it.

Here’s all of that in more details…

When should I use useReducer()?

  • When two or more states change together

An example of when states change together is when you fetch data. You might have fx. three states: isLoading, isError and data. Here’s a great article written by Robin Wieuruch on how to do data fetching with hooks. The second last part of his article shows how the data fetching component is refactored using useReducer() instead of useState(). (a sandbox based on his example).

useReducer():

const dataFetchReducer = (state, action) => {
  switch (action.type) {
    case "FETCH_INIT":
      return {
        ...state,
        isLoading: true,
        isError: false,
      }
    case "FETCH_SUCCESS":
      return {
        ...state,
        isLoading: false,
        isError: false,
        data: action.payload,
      }
    case "FETCH_FAILURE":
      return {
        ...state,
        isLoading: false,
        isError: true,
      }
    default:
      throw new Error()
  }
}

const useDataApi = (initialUrl, initialData) => {
  const [url, setUrl] = useState(initialUrl)

  const [state, dispatch] = useReducer(dataFetchReducer, {
    isLoading: false,
    isError: false,
    data: initialData,
  })

  useEffect(() => {
    const fetchData = async () => {
      dispatch({ type: "FETCH_INIT" })
      try {
        const result = await axios(url)
        dispatch({ type: "FETCH_SUCCESS", payload: result.data })
      } catch (error) {
        dispatch({ type: "FETCH_FAILURE" })
      }
    }
    fetchData()
  }, [url])
  return [state, setUrl]
}

Every time we fire an action there are two or three states changing at once.

If this would have been written with useState(), it would look like this:

const useDataApi = (initialUrl, initialData) => {
  const [data, setData] = useState(initialData)
  const [url, setUrl] = useState(initialUrl)
  const [isLoading, setIsLoading] = useState(false)
  const [isError, setIsError] = useState(false)
  useEffect(() => {
    const fetchData = async () => {
      setIsError(false)
      setIsLoading(true)
      try {
        const result = await axios(url)
        setData(result.data)
      } catch (error) {
        setIsError(true)
      }
      setIsLoading(false)
    }
    fetchData()
  }, [url])
  return [{ data, isLoading, isError }, setUrl]
}

The states, setData, setIsLoading, setIsError were all made to serve the same purpose, to achieve data fetching. So keeping all those states together, using useReducer(), is more readable and logical as they all change and belong together.

  • When your state relies on another state or previous state.

The example above relies on previous state. We made use of the former state to merge and overwrite it with the new state. Another example could be a todo app where the new state adds to the former state.

Exercise: Create a todo app where you can add and remove items, using useState() and then refactoring it using useReducer(). The Solution

When could I use useReducer()

This is the most tricky part because there are no clear guidelines on when to use it and when not to use it. You’ll have your own preferences

  • When you want to beautify / organize your code

This is mostly up to personal taste. Some find the code tidier when states are grouped into one useReducer().

Here’s a counter using useState():

function CounterState({ initialCount }) {
  const [count, setCount] = useState(0)
  return (
    <>
      useState() Count: {count}
      <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
      <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
    </>
  )
}

And here you have a counter using useReducer():

const initialState = { count: 0 }

function reducer(state, action) {
  switch (action.type) {
    case "increment":
      return { count: state.count + 1 }
    case "decrement":
      return { count: state.count - 1 }
    default:
      throw new Error()
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState)
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
    </>
  )
}

Both of these examples are taken from the React Doc so both of them are fine. The more states you add that should belong together, the more it makes sense to use useReducer(). Say for example that you’d add a “reset counter” action to start again counting from 0 –> then it’d make even more sense to use useReducer().

  • When your states get too complicated

You have this complex problem to solve with lots of logic. Perhaps your components have so many states that in the end you don’t get your head around it. useReducer() might help you reason about that logic.

  • When you want to do less “plumbing”

plumbing

Maybe you’ve already had to pass lots of props to another component. And that component has to pass it down to another component and that can go on and on… This is known as “plumbing” and can become quite cumbersome as if you have to change/remove/add props, you’d have to do that on each level and most probably update your typescript/proptypes as well. When all those states belong together, you could use useReducer() with either useContext() or React.createContext() to group them together and pass them down the line. Here’s an example from React’s official website.

  • When you’d like an easier way to make tests

Often when we use useState(), we’ll have to create a new function (called handlers) to take care of the logic that updates the state. Here’s a code snippet from a todo app using useState():

(Sandbox example of the todo app):

const [todo, setTodo] = useState();
 const [todos, setTodos] = useState([]);

function addTodo(value) {
  setTodos([...todos, { id: todos.length + 1, text: value ? value : todo }])
  setTodo("")
}

function removeTodo(id) {
  const newList = todos.filter(item => item.id !== id)
  setTodos(newList)

  ...
  <button onClick={() => addTodo()}>Add</button>
  ...
  <button onClick={() => removeTodo(todo.id)}>Remove</button>
  ...
}

Whereas when we used useReducer() in the todo app we call the same function, both for adding to the list or removing:

const [todoValue, setTodoValue] = useState();

function reducer(state, action) {
  switch (action.type) {
    case "remove":
      const newList = state.filter(item => item.id !== action.id)
      return newList
    case "add":
      setTodoValue("")
      return [...state, { id: state.length + 1, text: todoValue }]
    default:
      throw new Error()
  }
}

...
<button onClick={() => dispatch({ type: "add", text: todoValue })}>
...
<button onClick={() => dispatch({ type: "remove", id: todo.id })}>

Because there’s only one function, it becomes easier to test, easier to debug and also easier to reason about the component.

  • When you don’t know if you should use useReducer()

When you don’t have much experience with useReducer(), you can draft up your solution without useReducer() because useState() will always be able to get the job done and there’s nothing really wrong with it. You could always refactor it later.

When shouldn’t I use useReducer()?

  • When you have only individual state(s) in a component

Sometimes you only have one state in a component, or several states that don’t belong together. Here’s an example of a very simple toggle button:

disclaimer: turning radio button into a checkbox is a unique situation that you’ll probably never have to reproduce ever in real life

useState():

function ToggleButtonState({ initialCount }) {
  const [checked, toggle] = useState(false)

  return (
    <>
      useState() : Toggle the radio button
      <input
        type="radio"
        checked={checked}
        onChange={functionThatReturnsNull}
        onClick={() => toggle(!checked)}
      />
    </>
  )
}

vs. useReducer():

const initialState = { checked: false }

function reducer(state, action) {
  switch (action.type) {
    case "toggleCheck":
      return { checked: !state.checked }
    default:
      throw new Error()
  }
}

function ToggleButtonReducer() {
  const [state, dispatch] = useReducer(reducer, initialState)
  return (
    <>
      useReducer() : Toggle the radio button
      <input
        type="radio"
        checked={state.checked}
        onChange={functionThatReturnsNull}
        onClick={() => dispatch({ type: "toggleCheck" })}
      />
    </>
  )
}

Here useReducer() is quite an overkill. (The above code in CodeSandbox)

Summary

So now you’ve the powers to use useReducer() efficiently in your React apps which can save lots of headaches. If you’re not familiar with the javascript reducer() method or redux, it might take some time to put it into action but as you see, it’s definitely worth it. It can be a time saver and levels up your code!


Get more tips in my almost-weekly newsletter about CSS & React and other things, JavaScript and front-end related