React

Last Updated: 4/13/2023

Reducer

  • Components with many state updates spread across many event handlers can get overwhelming. For these cases, you can consolidate all the state update logic outside your component in a single function, called a reducer.
  • Reducers are a different way to handle state.
  • You can migrate from useState to useReducer in three steps:
    • Move from setting state to dispatching actions.
    • Write a reducer function.
    • Use the reducer from your component.

Without Reducer

TaskApp

let nextId = 3;
const initialTasks = [
  {id: 0, text: 'Buy Groceries', done: true},
  {id: 1, text: 'Learn React', done: false},
  {id: 2, text: 'Do Exercise', done: false},
];

export default function TaskApp() {
  const [tasks, setTasks] = useState(initialTasks);

  function handleAddTask(text) {
    setTasks([
      ...tasks,
      {
        id: nextId++,
        text: text,
        done: false,
      },
    ]);
  }

  function handleChangeTask(task) {
    setTasks(
      tasks.map((t) => {
        if (t.id === task.id) {
          return task;
        } else {
          return t;
        }
      })
    );
  }

  function handleDeleteTask(taskId) {
    setTasks(tasks.filter((t) => t.id !== taskId));
  }

  return (
    <>
      <h1>Task List</h1>
      <AddTask onAddTask={handleAddTask} />
      <TaskList
        tasks={tasks}
        onChangeTask={handleChangeTask}
        onDeleteTask={handleDeleteTask}
      />
    </>
  );
}

AddTask.js

export default function AddTask({onAddTask}) {
  const [text, setText] = useState('');
  return (
    <>
      <input
        placeholder="Add task"
        value={text}
        onChange={(e) => setText(e.target.value)}
      />
      <button
        onClick={() => {
          setText('');
          onAddTask(text);
        }}>
        Add
      </button>
    </>
  );
}

TaskList.js

export default function TaskList({tasks, onChangeTask, onDeleteTask}) {
  return (
    <ul>
      {tasks.map((task) => (
        <li key={task.id}>
          <Task task={task} onChange={onChangeTask} onDelete={onDeleteTask} />
        </li>
      ))}
    </ul>
  );
}

Task.js

function Task({task, onChange, onDelete}) {
  const [isEditing, setIsEditing] = useState(false);
  let taskContent;
  if (isEditing) {
    taskContent = (
      <>
        <input
          value={task.text}
          onChange={(e) => {
            onChange({
              ...task,
              text: e.target.value,
            });
          }}
        />
        <button onClick={() => setIsEditing(false)}>Save</button>
      </>
    );
  } else {
    taskContent = (
      <>
        {task.text}
        <button onClick={() => setIsEditing(true)}>Edit</button>
      </>
    );
  }
  return (
    <label>
      <input
        type="checkbox"
        checked={task.done}
        onChange={(e) => {
          onChange({
            ...task,
            done: e.target.checked,
          });
        }}
      />
      {taskContent}
      <button onClick={() => onDelete(task.id)}>Delete</button>
    </label>
  );
}

Move from setting state to dispatching actions.

  • Instead of telling React “what to do” by setting state, you specify “what the user just did” by dispatching “actions” from your event handlers.
  • The object you pass to dispatch is called an “action”:
  • Action is a regular JavaScript object. You decide what to put in it, but generally it should contain the minimal information about what happened.
const addTask = () => {
    dispatch({ type: "added", id: nextId++, text: "test" });
  };

const updateTask = (task: any) => {
  dispatch({ type: "changed", task: task });
};

const deleteTask = (id: any) => {
  dispatch({ type: "deleted", id: id });
};

Write a reducer function.

  • A reducer function is where you will put your state logic. It takes two arguments, the current state and the action object, and it returns the next state
  • Reducers must be pure.
  • Each action describes a single user interaction, even if that leads to multiple changes in the data
function tasksReducer(tasks: any, action: any) {
  switch (action.type) {
    case "added": {
      return [...tasks, { id: action.id, text: action.text, done: false }];
    }
    case "changed": {
      return tasks.map((t: any) => (t.id === action.task.id ? action.task : t));
    }
    case "deleted": {
      return tasks.filter((t: any) => t.id !== action.id);
    }
    default: {
      throw "Invalid action type";
    }
  }

Use the reducer from your component.

  • useReducertakes two arguments a reducer function and an initial state
  • useReducerreturns a stateful value and a dispatch function (to “dispatch” user actions to the reducer)
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);