DEV Community

MZ
MZ

Posted on

Reducers

We usually use reducers when we start having a bunch of setState() growing all over the codebase. It is also a way to manage the logic and put them into one place.

Consider the code below.

import { useReducer, useState } from 'react';

const App = () => {

  const [tasks, setTasks] = [
    {id: 0, text: 'Visit Museum', done: true},
    {id: 1, text: 'Watch a puppet show', done: false},
    {id: 2, text: 'Lennon Wall pic', done: false},
  ]

  const [inputText, setInputText] = useState('')

  const onInputTextChange = (e) => {
    setInputText(e.target.value)
  }

  const handleTaskAdd = () => {
    // setting state here
    setTasks(
      [
        ...tasks,
        {id: tasks.length, text: inputText, done: false}
      ]
    )

    setInputText('')
  }

  const handleTaskDelete = (taskId) => {
    // setting state here
    setTasks(tasks.filter(task => task.id !== taskId))
  }

  return (
    <div>
      <input type="text" value={inputText} onChange={onInputTextChange} />
      <button onClick={handleTaskAdd}>add task</button>

      <ul>
        {tasks.map(task => {
          return(
            <div key={task.id}>
              <p>{task.text}</p>
              <button onClick={() => handleTaskDelete(task.id)}>Delete</button>
            </div>
          )
        })}
      </ul>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

As you can see, there are some logic attached to the setState() portions of the code. This looks uncluttered and unmessy now, but when the codebase becomes larger, this is going to be an issue.

The main idea behind reducers is to, well, reduce. We essentially separate out the logic and state management into a part on its own.

The reducer holds and/or accepts 2 things.

  • tasks is the current state
  • action is the object relayed from the dispatch call.

As you can see in the code below, action carries several things, what type of change it is and the payload itself (or the content of said change).

const taskReducer = (tasks, action) => {
  switch (action.type) {
      case 'add': {
        const arr = [
          ...tasks,
          {id: tasks.length, text: action.text, done: false}
        ]
        return arr
      }
      case 'delete': {
        const arr = tasks.filter(task => task.id !== action.taskId)
        return arr
      }
      default: {
        throw Error(`Unknown error: ${action.type}`)
      }
  }
}
Enter fullscreen mode Exit fullscreen mode

Reducers are a mindset shift. Instead of setting states, we are sending out "actions".

In reducers, we mostly interact with the actions object.

  • we check what type of change it is
  • we deal with the logic and return the new state (we are utilising the tasks state in the logic here)

So, we're done with the main logic. How do we utilise it?

In the file that we would like to use the reducer in, import useReducer.

import { useReducer, useState } from 'react';

In the component that you are planning to use reducers in, do this.

const initialTasks = [
  {id: 0, text: 'Visit Museum', done: true},
  {id: 1, text: 'Watch a puppet show', done: false},
  {id: 2, text: 'Lennon Wall pic', done: false},
]

const [tasks, dispatch] = useReducer(taskReducer, initialTasks)
Enter fullscreen mode Exit fullscreen mode

Now, you have the initial data, initialTasks.

useReducer takes in the reducer itself and the initial state to hold and it returns [tasks, dispatch]. Notice something familiar?

we just turned

const [tasks, setTasks] = [
  {id: 0, text: 'Visit Museum', done: true},
  {id: 1, text: 'Watch a puppet show', done: false},
  {id: 2, text: 'Lennon Wall pic', done: false},
]
Enter fullscreen mode Exit fullscreen mode

into

const [tasks, dispatch] = useReducer(taskReducer, initialTasks)

Same mechanics.

Now, instead of:

  • dealing with the logic in the same file or line of code, you outsource it to the reducer
  • setting tasks using setTasks, you call dispatch

This is the updated code.

import { useReducer, useState } from 'react';

const App = () => {

  const initialTasks = [
    {id: 0, text: 'Visit Museum', done: true},
    {id: 1, text: 'Watch a puppet show', done: false},
    {id: 2, text: 'Lennon Wall pic', done: false},
  ]

  const [tasks, dispatch] = useReducer(taskReducer, initialTasks)

  const [inputText, setInputText] = useState('')

  const onInputTextChange = (e) => {
    setInputText(e.target.value)
  }

  const handleTaskAdd = () => {
    // copy array and append from there
    // setTasks(
    //   [
    //     ...tasks,
    //     {id: tasks.length, text: inputText, done: false}
    //   ]
    // )

    dispatch({
      type: 'add',
      text: inputText
    })
    setInputText('')
  }

  const handleTaskDelete = (taskId) => {
    // setTasks(tasks.filter(task => task.id !== taskId))
    dispatch({
      type: 'delete',
      taskId: taskId
    })
  }

  return (
    <div>
      <input type="text" value={inputText} onChange={onInputTextChange} />
      <button onClick={handleTaskAdd}>add task</button>

      <ul>
        {tasks.map(task => {
          return(
            <div key={task.id}>
              <p>{task.text}</p>
              <button onClick={() => handleTaskDelete(task.id)}>Delete</button>
            </div>
          )
        })}
      </ul>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Notice the dispatch() calls?

Instead of a bunch of setState(), we dispatch.

dispatch() usually sends an object consisting of:

  • the type of action
  • the payload (text etc)

An example pseudocode:

dispatch({
  type: 'add',
  text: inputText
})
Enter fullscreen mode Exit fullscreen mode

Every time we call dispatch() with the action object along with it, we are essentially calling the reducer that we separated into a part on its own. That reducer would then deal with the logic.

Remember this line from the main file?
const [tasks, dispatch] = useReducer(taskReducer, initialTasks)

initialTasks is fed into tasks as the current state.

Whenever we call the reducer, it handles the logic and returns a new state back to the tasks state variable.

What we have essentially done, is to streamline the state logic.

Do you use reducers all the time? Not really. The general rule of thumb is to use reducers if state starts getting complicated. If it stays simple, use useState().

Top comments (0)