Вилучення логіки станів у редуктор

Компоненти з великою кількістю оновлень стану, розподілених між багатьма обробниками подій, можуть бути перевантажені. У таких випадках ви можете винести всю логіку оновлення станів за межі вашого компонента у єдину функцію, яка називається редуктор.

  • Що таке функція-редуктор
  • Як рефакторити useState в useReducer
  • Коли потрібно використовувати редуктор
  • Як правильно написати один well

Консолідація логіки станів з редуктором

З ускладненням ваших компонентів може стати важче бачити з першого погляду всі різні способи оновлення стану компонента. Наприклад, компонент TaskApp нижче містить масив завдань у стані і використовує три різні обробники подій для додавання, видалення та редагування завдань:

import { useState } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';

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>Prague itinerary</h1>
      <AddTask onAddTask={handleAddTask} />
      <TaskList
        tasks={tasks}
        onChangeTask={handleChangeTask}
        onDeleteTask={handleDeleteTask}
      />
    </>
  );
}

let nextId = 3;
const initialTasks = [
  {id: 0, text: 'Visit Kafka Museum', done: true},
  {id: 1, text: 'Watch a puppet show', done: false},
  {id: 2, text: 'Lennon Wall pic', done: false},
];
import { useState } from 'react';

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>
    </>
  );
}
import { useState } from 'react';

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>
  );
}

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>
  );
}
button {
  margin: 5px;
}
li {
  list-style-type: none;
}
ul,
li {
  margin: 0;
  padding: 0;
}

Кожен з його обробників подій викликає setTasks для оновлення стану. Зі зростанням цього компонента зростає і кількість логіки станів, що розкидана по ньому. Щоб зменшити цю складність і тримати всю логіку в одному легкодоступному місці, ви можете винести цю логіку станів в окрему функцію за межі вашого компонента, яка називається "редуктором".

Редуктори - це інший спосіб обробки стану. Ви можете мігрувати з useState до useReducer у три кроки:

  1. Перемістити зі стану встановлення до відправлення дій.
  2. Напишіть функцію-редуктор.
  3. Використовуйте редуктор з вашого компонента.

Крок 1: Перехід від встановлення стану до відправлення дій

Ваші обробники подій наразі визначають що робити шляхом встановлення стану:

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));
}

Видаліть усю логіку встановлення станів. У вас залишиться три обробники подій:

  • handleAddTask(text) викликається при натисканні користувачем кнопки "Додати".
  • handleChangeTask(task) викликається, коли користувач перемикає завдання або натискає "Зберегти".
  • handleDeleteTask(taskId) викликається, коли користувач натискає клавішу "Видалити".

Керування станом за допомогою редукторів дещо відрізняється від безпосереднього встановлення стану. Замість того, щоб вказувати React "що робити", встановлюючи стан, ви вказуєте "що користувач щойно зробив", відправляючи "дії" з ваших обробників подій. (Логіка оновлення стану буде жити деінде!) Таким чином, замість "встановлення завдань" через обробник події, ви відправляєте дію "додано/змінено/видалено завдання". Це краще описує наміри користувача.

function handleAddTask(text) {
  dispatch({
    type: 'added',
    id: nextId++,
    text: text,
  });
}

function handleChangeTask(task) {
  dispatch({
    type: 'changed',
    task: task,
  });
}

function handleDeleteTask(taskId) {
  dispatch({
    type: 'deleted',
    id: taskId,
  });
}

Об'єкт, який ви передаєте до диспетчера, називається "дією":

function handleDeleteTask(taskId) {
  dispatch(
    // "action" object:
    {
      type: 'deleted',
      id: taskId,
    }
  );
}

Це звичайний об'єкт JavaScript. Ви самі вирішуєте, що до нього помістити, але загалом він має містити мінімальну інформацію про те, що сталося . (Саму функцію диспетчеризації ви додасте на наступному кроці).

Об'єкт дії може мати будь-яку форму.

За домовленістю, прийнято передавати йому рядок типу, який описує те, що сталося, а в інших полях передавати будь-яку додаткову інформацію. type є специфічним для компонента, тому у цьому прикладі підійде або 'added', або 'added_task'. Виберіть назву, яка говорить про те, що сталося!

dispatch({
  // specific to component
  type: 'what_happened',
  // other fields go here
});

Крок 2: Написання функції-редуктора

Функція-редуктор - це місце, де ви будете розміщувати свою логіку стану. Вона приймає два аргументи, поточний стан і об'єкт дії, і повертає наступний стан:

function yourReducer(state, action) {
  // return next state for React to set
}

React встановить стан на те, що ви повернете з редуктора.

Щоб перенести логіку встановлення стану з обробників подій до функції-зменшувача у цьому прикладі, вам слід:

  1. Оголосіть поточний стан (завдання) першим аргументом.
  2. Оголосіть об'єкт action як другий аргумент.
  3. Поверніть стан next з редуктора (на який React встановить стан).

Тут вся логіка встановлення стану перенесена у функцію-редуктор:

function tasksReducer(tasks, action) {
  if (action.type === 'added') {
    return [
      ...tasks,
      {
        id: action.id,
        text: action.text,
        done: false,
      },
    ];
  } else if (action.type === 'changed') {
    return tasks.map((t) => {
      if (t.id === action.task.id) {
        return action.task;
      } else {
        return t;
      }
    });
  } else if (action.type === 'deleted') {
    return tasks.filter((t) => t.id !== action.id);
  } else {
    throw Error('Unknown action: ' + action.type);
  }
}

Оскільки функція-редуктор отримує стан (завдання) як аргумент, ви можете оголосити її поза вашим компонентом. Це зменшує рівень відступів і може полегшити читання вашого коду.

Вищенаведений код використовує оператори if/else, але в редукторах прийнято використовувати оператори switch. Результат однаковий, але оператори switch легше читати з першого погляду.

Ми будемо використовувати їх у решті частини цієї документації таким чином:

function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added': {
      return [
        ...tasks,
        {
          id: action.id,
          text: action.text,
          done: false,
        },
      ];
    }
    case 'changed': {
      return tasks.map((t) => {
        if (t.id === action.task.id) {
          return action.task;
        } else {
          return t;
        }
      });
    }
    case 'deleted': {
      return tasks.filter((t) => t.id !== action.id);
    }
    default: {
      throw Error('Unknown action: ' + action.type);
    }
  }
}

Ми рекомендуємо обгортати кожен блок case у фігурні дужки { та }, щоб змінні, оголошені всередині різних case, не конфліктували між собою. Крім того, case зазвичай має закінчуватися return. Якщо ви забудете return, код "відкотиться" до наступного case, що може призвести до помилок!

Якщо ви ще не знайомі з операторами перемикання, використання if/else є цілком прийнятним.

Чому редуктори так називаються?

Хоча редуктори можуть "зменшити" кількість коду всередині вашого компонента, насправді вони названі на честь операції reduce(), яку ви можете виконувати над масивами.

Операція reduce() дозволяє взяти масив і "накопичити" одне значення з багатьох:

const arr = [1, 2, 3, 4, 5];
const sum = arr.reduce(
  (result, number) => result + number
); // 1 + 2 + 3 + 4 + 5

Функція, яку ви передаєте до reduce, відома як "редуктор". Вона бере результат дотепер і поточний елемент, після чого повертає наступний результат. Редуктори React є прикладом тієї ж ідеї: вони беруть стан поки що і дію, і повертають наступний стан. Таким чином, вони накопичують дії з часом у стан.

Ви навіть можете використати метод reduce() з initialState та масивом дій для обчислення кінцевого стану, передавши йому вашу функцію-редуктор:

import tasksReducer from './tasksReducer.js';

let initialState = [];
let actions = [
  {type: 'added', id: 1, text: 'Visit Kafka Museum'},
  {type: 'added', id: 2, text: 'Watch a puppet show'},
  {type: 'deleted', id: 1},
  {type: 'added', id: 3, text: 'Lennon Wall pic'},
];

let finalState = actions.reduce(tasksReducer, initialState);

const output = document.getElementById('output');
output.textContent = JSON.stringify(finalState, null, 2);
export default function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added': {
      return [
        ...tasks,
        {
          id: action.id,
          text: action.text,
          done: false,
        },
      ];
    }
    case 'changed': {
      return tasks.map((t) => {
        if (t.id === action.task.id) {
          return action.task;
        } else {
          return t;
        }
      });
    }
    case 'deleted': {
      return tasks.filter((t) => t.id !== action.id);
    }
    default: {
      throw Error('Unknown action: ' + action.type);
    }
  }
}
<pre id="output"></pre>

Вам, ймовірно, не знадобиться робити це самостійно, але це схоже на те, що робить React!

Крок 3: Використання редуктора з вашого компонента

Нарешті, вам потрібно підключити tasksReducer до вашого компонента. Імпортуйте хук useReducer з React:

import { useReducer } from 'react';

Тоді ви можете замінити useState:

const [tasks, setTasks] = useState(initialTasks);

з useReducer ось так:

const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

Хук useReducer схожий на useState - ви маєте передати йому початковий стан, а він повертає значення стану та спосіб встановлення стану (у цьому випадку функцію відправки). Але це дещо інше.

Хук useReducer приймає два аргументи:

  1. Редукторна функція
  2. Початковий стан

І повертає:

  1. Значення зі статусом
  2. Функція відправки (для "відправки" дій користувача до редуктора)

Тепер він повністю підключений! Тут редуктор оголошено внизу файлу компонента:

import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';

export default function TaskApp() {
  const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

  function handleAddTask(text) {
    dispatch({
      type: 'added',
      id: nextId++,
      text: text,
    });
  }

  function handleChangeTask(task) {
    dispatch({
      type: 'changed',
      task: task,
    });
  }

  function handleDeleteTask(taskId) {
    dispatch({
      type: 'deleted',
      id: taskId,
    });
  }

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

function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added': {
      return [
        ...tasks,
        {
          id: action.id,
          text: action.text,
          done: false,
        },
      ];
    }
    case 'changed': {
      return tasks.map((t) => {
        if (t.id === action.task.id) {
          return action.task;
        } else {
          return t;
        }
      });
    }
    case 'deleted': {
      return tasks.filter((t) => t.id !== action.id);
    }
    default: {
      throw Error('Unknown action: ' + action.type);
    }
  }
}

let nextId = 3;
const initialTasks = [
  {id: 0, text: 'Visit Kafka Museum', done: true},
  {id: 1, text: 'Watch a puppet show', done: false},
  {id: 2, text: 'Lennon Wall pic', done: false},
];
import { useState } from 'react';

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>
    </>
  );
}
import { useState } from 'react';

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>
  );
}

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>
  );
}
button {
  margin: 5px;
}
li {
  list-style-type: none;
}
ul,
li {
  margin: 0;
  padding: 0;
}

Якщо хочете, можете навіть перемістити редуктор в інший файл:

import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
import tasksReducer from './tasksReducer.js';

export default function TaskApp() {
  const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

  function handleAddTask(text) {
    dispatch({
      type: 'added',
      id: nextId++,
      text: text,
    });
  }

  function handleChangeTask(task) {
    dispatch({
      type: 'changed',
      task: task,
    });
  }

  function handleDeleteTask(taskId) {
    dispatch({
      type: 'deleted',
      id: taskId,
    });
  }

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

let nextId = 3;
const initialTasks = [
  {id: 0, text: 'Visit Kafka Museum', done: true},
  {id: 1, text: 'Watch a puppet show', done: false},
  {id: 2, text: 'Lennon Wall pic', done: false},
];
export default function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added': {
      return [
        ...tasks,
        {
          id: action.id,
          text: action.text,
          done: false,
        },
      ];
    }
    case 'changed': {
      return tasks.map((t) => {
        if (t.id === action.task.id) {
          return action.task;
        } else {
          return t;
        }
      });
    }
    case 'deleted': {
      return tasks.filter((t) => t.id !== action.id);
    }
    default: {
      throw Error('Unknown action: ' + action.type);
    }
  }
}
import { useState } from 'react';

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>
    </>
  );
}
import { useState } from 'react';

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>
  );
}

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>
  );
}
button {
  margin: 5px;
}
li {
  list-style-type: none;
}
ul,
li {
  margin: 0;
  padding: 0;
}

Логіку компонентів легше читати, коли ви відокремлюєте такі проблеми. Тепер обробники подій лише визначають що сталося, надсилаючи дії, а функція редуктора визначає як оновлюється стан у відповідь на них.

Порівняння useState та useReducer

Редуктори не позбавлені недоліків! Ось кілька способів їх порівняння:

  • Розмір коду: Як правило, з useState вам доведеться писати менше коду наперед. З useReducer вам доведеться написати як функцію-редуктор , так і дії відправки . Однак, useReducer може допомогти скоротити код, якщо багато обробників подій змінюють стан у схожий спосіб.
  • Читабельність: useState дуже легко читати, коли оновлення стану прості. Коли вони стають складнішими, вони можуть роздути код вашого компонента і ускладнити його сканування. У цьому випадку useReducer дозволяє чітко відокремити як логіку оновлення від що сталося обробників подій.
  • Відлагодження: Коли у вас є вада з useState, може бути важко сказати де було встановлено неправильний стан, і чому. За допомогою useReducer ви можете додати консольний лог до вашого редуктора, щоб бачити кожне оновлення стану і чому це сталося (через яку дію). Якщо кожна дія буде правильною, ви знатимете, що помилка у самій логіці редуктора. Однак, вам доведеться переглянути більше коду, ніж у випадку з useState.
  • .
  • Тестування: Редуктор - це чиста функція, яка не залежить від вашого компонента. Це означає, що ви можете експортувати і тестувати його окремо в ізоляції. Хоча зазвичай краще тестувати компоненти у більш реалістичному середовищі, для складної логіки оновлення стану може бути корисним стверджувати, що ваш редуктор повертає певний стан для певного початкового стану та дії.
  • Особисті вподобання: Комусь подобаються редуктори, комусь ні. Це нормально. Це справа вподобань. Ви завжди можете конвертувати між useState і useReducer туди і назад: вони еквівалентні!
  • Ми рекомендуємо використовувати редуктор, якщо ви часто стикаєтеся з помилками через некоректне оновлення станів у якомусь компоненті, і хочете внести більше структури у його код. Вам не обов'язково використовувати редуктори для всього: не соромтеся змішувати і поєднувати! Ви навіть можете використовувати useState і useReducer в одному компоненті.

    Добре пишемо редуктори

    Памʼятайте про ці дві поради під час написання редукторів:

    • Редуктори мають бути чистими. Подібно до функцій оновлення стану, редуктори працюють під час рендерингу! (Дії чекають у черзі до наступного рендерингу). Це означає, що редуктори мають бути чистими - ті самі вхідні дані завжди дають той самий вихід. Вони не повинні надсилати запити, призначати таймаути або виконувати будь-які побічні ефекти (операції, які впливають на речі за межами компонента). Вони повинні оновлювати об'єкти та масиви без мутацій.
    • Кожна дія описує одну взаємодію користувача, навіть якщо це призводить до кількох змін у даних. Наприклад, якщо користувач натискає кнопку "Скинути" у формі з п'ятьма полями, керованими редуктором, має сенс виконати одну дію reset_form, а не п'ять окремих set_field. Якщо ви записуєте кожну дію у редукторі, цей лог має бути достатньо чітким для того, щоб ви могли відстежити, які взаємодії або відповіді відбувалися у якому порядку. Це допоможе у налагодженні!

    Написання стислих редукторів за допомогою Immer

    Так само, як і у випадку з оновленням об'єктів та масивів у звичайному стані, ви можете використати бібліотеку Immer, щоб зробити редуктори більш лаконічними. Тут useImmerReducer дозволяє мутувати стан за допомогою push або задачі arr[i] =:

    import { useImmerReducer } from 'use-immer';
    import AddTask from './AddTask.js';
    import TaskList from './TaskList.js';
    
    function tasksReducer(draft, action) {
      switch (action.type) {
        case 'added': {
          draft.push({
            id: action.id,
            text: action.text,
            done: false,
          });
          break;
        }
        case 'changed': {
          const index = draft.findIndex((t) => t.id === action.task.id);
          draft[index] = action.task;
          break;
        }
        case 'deleted': {
          return draft.filter((t) => t.id !== action.id);
        }
        default: {
          throw Error('Unknown action: ' + action.type);
        }
      }
    }
    
    export default function TaskApp() {
      const [tasks, dispatch] = useImmerReducer(tasksReducer, initialTasks);
    
      function handleAddTask(text) {
        dispatch({
          type: 'added',
          id: nextId++,
          text: text,
        });
      }
    
      function handleChangeTask(task) {
        dispatch({
          type: 'changed',
          task: task,
        });
      }
    
      function handleDeleteTask(taskId) {
        dispatch({
          type: 'deleted',
          id: taskId,
        });
      }
    
      return (
        <>
          <h1>Prague itinerary</h1>
          <AddTask onAddTask={handleAddTask} />
          <TaskList
            tasks={tasks}
            onChangeTask={handleChangeTask}
            onDeleteTask={handleDeleteTask}
          />
        </>
      );
    }
    
    let nextId = 3;
    const initialTasks = [
      {id: 0, text: 'Visit Kafka Museum', done: true},
      {id: 1, text: 'Watch a puppet show', done: false},
      {id: 2, text: 'Lennon Wall pic', done: false},
    ];
    import { useState } from 'react';
    
    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>
        </>
      );
    }
    import { useState } from 'react';
    
    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>
      );
    }
    
    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>
      );
    }
    button {
      margin: 5px;
    }
    li {
      list-style-type: none;
    }
    ul,
    li {
      margin: 0;
      padding: 0;
    }
    {
      "dependencies": {
        "immer": "1.7.3",
        "react": "latest",
        "react-dom": "latest",
        "react-scripts": "latest",
        "use-immer": "0.5.1"
      },
      "scripts": {
        "start": "react-scripts start",
        "build": "react-scripts build",
        "test": "react-scripts test --env=jsdom",
        "eject": "react-scripts eject"
      }
    }

    Редуктори мають бути чистими, тому вони не повинні мутувати стан. Але Immer надає вам спеціальний об'єкт draft, який безпечно мутувати. За лаштунками Immer створить копію вашого стану зі змінами, які ви внесли до draft. Ось чому редуктори, керовані useImmerReducer, можуть змінювати свій перший аргумент і не потребують повернення стану.

    • Перетворити з useState на useReducer:
      1. Розсилка дій з обробників подій.
      2. Напишіть функцію-редуктор, яка повертає наступний стан для заданих стану та дії.
      3. Замінити useState на useReducer.
    • Редуктори вимагають написання трохи більшого обсягу коду, але вони допомагають у налагодженні та тестуванні
    • Редуктори мають бути чистими.
    • Кожна дія описує одну взаємодію користувача.
    • Використовуйте Immer, якщо ви хочете писати редуктори у стилі мутації.

    Диспетчеризація дій з обробників подій

    Наразі обробники подій у ContactList.js та Chat.js мають // TODO коментарі. Ось чому введення даних не працює, а натискання кнопок не змінює обраного отримувача.

    Замініть ці два // TODO на код для dispatch відповідних дій. Щоб побачити очікувану форму і тип дій, перевірте редуктор у messengerReducer.js. Редуктор вже написано, тому вам не потрібно буде його змінювати. Вам потрібно лише відправити дії у ContactList.js та Chat.js.

    Функція dispatch вже доступна в обох цих компонентах, оскільки вона була передана як проп. Тому вам потрібно викликати dispatch з відповідним об'єктом дії.

    Щоб перевірити форму об'єкта дії, ви можете подивитися на редуктор і побачити, які поля дії він очікує побачити. Наприклад, випадок changed_selection у редукторі виглядає так:

    case 'changed_selection': {
      return {
        ...state,
        selectedId: action.contactId
      };
    }

    Це означає, що ваш об'єкт дії повинен мати type: 'changed_selection'. Ви також бачите, що використовується action.contactId, тому вам слід додати властивість contactId до вашої дії.

    import { useReducer } from 'react';
    import Chat from './Chat.js';
    import ContactList from './ContactList.js';
    import { initialState, messengerReducer } from './messengerReducer';
    
    export default function Messenger() {
      const [state, dispatch] = useReducer(messengerReducer, initialState);
      const message = state.message;
      const contact = contacts.find((c) => c.id === state.selectedId);
      return (
        <div>
          <ContactList
            contacts={contacts}
            selectedId={state.selectedId}
            dispatch={dispatch}
          />
          <Chat
            key={contact.id}
            message={message}
            contact={contact}
            dispatch={dispatch}
          />
        </div>
      );
    }
    
    const contacts = [
      {id: 0, name: 'Taylor', email: '[email protected]'},
      {id: 1, name: 'Alice', email: '[email protected]'},
      {id: 2, name: 'Bob', email: '[email protected]'},
    ];
    export const initialState = {
      selectedId: 0,
      message: 'Hello',
    };
    
    export function messengerReducer(state, action) {
      switch (action.type) {
        case 'changed_selection': {
          return {
            ...state,
            selectedId: action.contactId,
            message: '',
          };
        }
        case 'edited_message': {
          return {
            ...state,
            message: action.message,
          };
        }
        default: {
          throw Error('Unknown action: ' + action.type);
        }
      }
    }
    export default function ContactList({contacts, selectedId, dispatch}) {
      return (
        <section className="contact-list">
          <ul>
            {contacts.map((contact) => (
              <li key={contact.id}>
                <button
                  onClick={() => {
                    // TODO: dispatch changed_selection
                  }}>
                  {selectedId === contact.id ? <b>{contact.name}</b> : contact.name}
                </button>
              </li>
            ))}
          </ul>
        </section>
      );
    }
    import { useState } from 'react';
    
    export default function Chat({contact, message, dispatch}) {
      return (
        <section className="chat">
          <textarea
            value={message}
            placeholder={'Chat to ' + contact.name}
            onChange={(e) => {
              // TODO: dispatch edited_message
              // (Read the input value from e.target.value)
            }}
          />
          <br />
          <button>Send to {contact.email}</button>
        </section>
      );
    }
    .chat,
    .contact-list {
      float: left;
      margin-bottom: 20px;
    }
    ul,
    li {
      list-style: none;
      margin: 0;
      padding: 0;
    }
    li button {
      width: 100px;
      padding: 10px;
      margin-right: 10px;
    }
    textarea {
      height: 150px;
    }

    З коду редуктора можна зробити висновок, що дії мають виглядати так:

    // When the user presses "Alice"
    dispatch({
      type: 'changed_selection',
      contactId: 1,
    });
    
    // When user types "Hello!"
    dispatch({
      type: 'edited_message',
      message: 'Hello!',
    });

    Ось приклад, оновлений для надсилання відповідних повідомлень:

    import { useReducer } from 'react';
    import Chat from './Chat.js';
    import ContactList from './ContactList.js';
    import { initialState, messengerReducer } from './messengerReducer';
    
    export default function Messenger() {
      const [state, dispatch] = useReducer(messengerReducer, initialState);
      const message = state.message;
      const contact = contacts.find((c) => c.id === state.selectedId);
      return (
        <div>
          <ContactList
            contacts={contacts}
            selectedId={state.selectedId}
            dispatch={dispatch}
          />
          <Chat
            key={contact.id}
            message={message}
            contact={contact}
            dispatch={dispatch}
          />
        </div>
      );
    }
    
    const contacts = [
      {id: 0, name: 'Taylor', email: '[email protected]'},
      {id: 1, name: 'Alice', email: '[email protected]'},
      {id: 2, name: 'Bob', email: '[email protected]'},
    ];
    export const initialState = {
      selectedId: 0,
      message: 'Hello',
    };
    
    export function messengerReducer(state, action) {
      switch (action.type) {
        case 'changed_selection': {
          return {
            ...state,
            selectedId: action.contactId,
            message: '',
          };
        }
        case 'edited_message': {
          return {
            ...state,
            message: action.message,
          };
        }
        default: {
          throw Error('Unknown action: ' + action.type);
        }
      }
    }
    export default function ContactList({contacts, selectedId, dispatch}) {
      return (
        <section className="contact-list">
          <ul>
            {contacts.map((contact) => (
              <li key={contact.id}>
                <button
                  onClick={() => {
                    dispatch({
                      type: 'changed_selection',
                      contactId: contact.id,
                    });
                  }}>
                  {selectedId === contact.id ? <b>{contact.name}</b> : contact.name}
                </button>
              </li>
            ))}
          </ul>
        </section>
      );
    }
    import { useState } from 'react';
    
    export default function Chat({contact, message, dispatch}) {
      return (
        <section className="chat">
          <textarea
            value={message}
            placeholder={'Chat to ' + contact.name}
            onChange={(e) => {
              dispatch({
                type: 'edited_message',
                message: e.target.value,
              });
            }}
          />
          <br />
          <button>Send to {contact.email}</button>
        </section>
      );
    }
    .chat,
    .contact-list {
      float: left;
      margin-bottom: 20px;
    }
    ul,
    li {
      list-style: none;
      margin: 0;
      padding: 0;
    }
    li button {
      width: 100px;
      padding: 10px;
      margin-right: 10px;
    }
    textarea {
      height: 150px;
    }

    Очистити введення при надсиланні повідомлення

    Наразі натискання кнопки "Надіслати" нічого не робить. Додайте обробник події до кнопки "Надіслати", який:

    1. покаже сповіщення з електронною адресою одержувача та повідомленням.
    2. Очистити введення повідомлення.
    import { useReducer } from 'react';
    import Chat from './Chat.js';
    import ContactList from './ContactList.js';
    import { initialState, messengerReducer } from './messengerReducer';
    
    export default function Messenger() {
      const [state, dispatch] = useReducer(messengerReducer, initialState);
      const message = state.message;
      const contact = contacts.find((c) => c.id === state.selectedId);
      return (
        <div>
          <ContactList
            contacts={contacts}
            selectedId={state.selectedId}
            dispatch={dispatch}
          />
          <Chat
            key={contact.id}
            message={message}
            contact={contact}
            dispatch={dispatch}
          />
        </div>
      );
    }
    
    const contacts = [
      {id: 0, name: 'Taylor', email: '[email protected]'},
      {id: 1, name: 'Alice', email: '[email protected]'},
      {id: 2, name: 'Bob', email: '[email protected]'},
    ];
    export const initialState = {
      selectedId: 0,
      message: 'Hello',
    };
    
    export function messengerReducer(state, action) {
      switch (action.type) {
        case 'changed_selection': {
          return {
            ...state,
            selectedId: action.contactId,
            message: '',
          };
        }
        case 'edited_message': {
          return {
            ...state,
            message: action.message,
          };
        }
        default: {
          throw Error('Unknown action: ' + action.type);
        }
      }
    }
    export default function ContactList({contacts, selectedId, dispatch}) {
      return (
        <section className="contact-list">
          <ul>
            {contacts.map((contact) => (
              <li key={contact.id}>
                <button
                  onClick={() => {
                    dispatch({
                      type: 'changed_selection',
                      contactId: contact.id,
                    });
                  }}>
                  {selectedId === contact.id ? <b>{contact.name}</b> : contact.name}
                </button>
              </li>
            ))}
          </ul>
        </section>
      );
    }
    import { useState } from 'react';
    
    export default function Chat({contact, message, dispatch}) {
      return (
        <section className="chat">
          <textarea
            value={message}
            placeholder={'Chat to ' + contact.name}
            onChange={(e) => {
              dispatch({
                type: 'edited_message',
                message: e.target.value,
              });
            }}
          />
          <br />
          <button>Send to {contact.email}</button>
        </section>
      );
    }
    .chat,
    .contact-list {
      float: left;
      margin-bottom: 20px;
    }
    ul,
    li {
      list-style: none;
      margin: 0;
      padding: 0;
    }
    li button {
      width: 100px;
      padding: 10px;
      margin-right: 10px;
    }
    textarea {
      height: 150px;
    }

    Є кілька способів зробити це в обробнику події кнопки "Надіслати". Один з підходів полягає у показі попередження, а потім відправленні дії edited_message з порожнім повідомленням :

    import { useReducer } from 'react';
    import Chat from './Chat.js';
    import ContactList from './ContactList.js';
    import { initialState, messengerReducer } from './messengerReducer';
    
    export default function Messenger() {
      const [state, dispatch] = useReducer(messengerReducer, initialState);
      const message = state.message;
      const contact = contacts.find((c) => c.id === state.selectedId);
      return (
        <div>
          <ContactList
            contacts={contacts}
            selectedId={state.selectedId}
            dispatch={dispatch}
          />
          <Chat
            key={contact.id}
            message={message}
            contact={contact}
            dispatch={dispatch}
          />
        </div>
      );
    }
    
    const contacts = [
      {id: 0, name: 'Taylor', email: '[email protected]'},
      {id: 1, name: 'Alice', email: '[email protected]'},
      {id: 2, name: 'Bob', email: '[email protected]'},
    ];
    export const initialState = {
      selectedId: 0,
      message: 'Hello',
    };
    
    export function messengerReducer(state, action) {
      switch (action.type) {
        case 'changed_selection': {
          return {
            ...state,
            selectedId: action.contactId,
            message: '',
          };
        }
        case 'edited_message': {
          return {
            ...state,
            message: action.message,
          };
        }
        default: {
          throw Error('Unknown action: ' + action.type);
        }
      }
    }
    export default function ContactList({contacts, selectedId, dispatch}) {
      return (
        <section className="contact-list">
          <ul>
            {contacts.map((contact) => (
              <li key={contact.id}>
                <button
                  onClick={() => {
                    dispatch({
                      type: 'changed_selection',
                      contactId: contact.id,
                    });
                  }}>
                  {selectedId === contact.id ? <b>{contact.name}</b> : contact.name}
                </button>
              </li>
            ))}
          </ul>
        </section>
      );
    }
    import { useState } from 'react';
    
    export default function Chat({contact, message, dispatch}) {
      return (
        <section className="chat">
          <textarea
            value={message}
            placeholder={'Chat to ' + contact.name}
            onChange={(e) => {
              dispatch({
                type: 'edited_message',
                message: e.target.value,
              });
            }}
          />
          <br />
          <button
            onClick={() => {
              alert(`Sending "${message}" to ${contact.email}`);
              dispatch({
                type: 'edited_message',
                message: '',
              });
            }}>
            Send to {contact.email}
          </button>
        </section>
      );
    }
    .chat,
    .contact-list {
      float: left;
      margin-bottom: 20px;
    }
    ul,
    li {
      list-style: none;
      margin: 0;
      padding: 0;
    }
    li button {
      width: 100px;
      padding: 10px;
      margin-right: 10px;
    }
    textarea {
      height: 150px;
    }

    Це працює і очищає введення, коли ви натискаєте "Надіслати".

    Втім, з точки зору користувача, надсилання повідомлення відрізняється від редагування поля. Щоб відобразити це, ви можете замість цього створити нову дію з назвою sent_message і обробити її окремо у редукторі:

    import { useReducer } from 'react';
    import Chat from './Chat.js';
    import ContactList from './ContactList.js';
    import { initialState, messengerReducer } from './messengerReducer';
    
    export default function Messenger() {
      const [state, dispatch] = useReducer(messengerReducer, initialState);
      const message = state.message;
      const contact = contacts.find((c) => c.id === state.selectedId);
      return (
        <div>
          <ContactList
            contacts={contacts}
            selectedId={state.selectedId}
            dispatch={dispatch}
          />
          <Chat
            key={contact.id}
            message={message}
            contact={contact}
            dispatch={dispatch}
          />
        </div>
      );
    }
    
    const contacts = [
      {id: 0, name: 'Taylor', email: '[email protected]'},
      {id: 1, name: 'Alice', email: '[email protected]'},
      {id: 2, name: 'Bob', email: '[email protected]'},
    ];
    export const initialState = {
      selectedId: 0,
      message: 'Hello',
    };
    
    export function messengerReducer(state, action) {
      switch (action.type) {
        case 'changed_selection': {
          return {
            ...state,
            selectedId: action.contactId,
            message: '',
          };
        }
        case 'edited_message': {
          return {
            ...state,
            message: action.message,
          };
        }
        case 'sent_message': {
          return {
            ...state,
            message: '',
          };
        }
        default: {
          throw Error('Unknown action: ' + action.type);
        }
      }
    }
    export default function ContactList({contacts, selectedId, dispatch}) {
      return (
        <section className="contact-list">
          <ul>
            {contacts.map((contact) => (
              <li key={contact.id}>
                <button
                  onClick={() => {
                    dispatch({
                      type: 'changed_selection',
                      contactId: contact.id,
                    });
                  }}>
                  {selectedId === contact.id ? <b>{contact.name}</b> : contact.name}
                </button>
              </li>
            ))}
          </ul>
        </section>
      );
    }
    import { useState } from 'react';
    
    export default function Chat({contact, message, dispatch}) {
      return (
        <section className="chat">
          <textarea
            value={message}
            placeholder={'Chat to ' + contact.name}
            onChange={(e) => {
              dispatch({
                type: 'edited_message',
                message: e.target.value,
              });
            }}
          />
          <br />
          <button
            onClick={() => {
              alert(`Sending "${message}" to ${contact.email}`);
              dispatch({
                type: 'sent_message',
              });
            }}>
            Send to {contact.email}
          </button>
        </section>
      );
    }
    .chat,
    .contact-list {
      float: left;
      margin-bottom: 20px;
    }
    ul,
    li {
      list-style: none;
      margin: 0;
      padding: 0;
    }
    li button {
      width: 100px;
      padding: 10px;
      margin-right: 10px;
    }
    textarea {
      height: 150px;
    }

    Поведінка буде однаковою. Але майте на увазі, що типи дій в ідеалі повинні описувати "що зробив користувач", а не "як ви хочете, щоб стан змінився". Це полегшить подальше додавання нових можливостей.

    У будь-якому випадку важливо, щоб ви не розміщували попередження всередині редуктора. Редуктор має бути чистою функцією - він повинен лише обчислювати наступний стан. Він не повинен нічого "робити", у тому числі виводити повідомлення користувачеві. Це має відбуватися в обробнику події. (Щоб допомогти виявити подібні помилки, React буде викликати ваші редуктори багато разів у строгому режимі. Ось чому, якщо ви поміщаєте попередження в редуктор, воно спрацьовує двічі)

    .

    Відновлювати значення вводу при перемиканні між вкладками

    У цьому прикладі перемикання між різними отримувачами завжди очищає введений текст:

    case 'changed_selection': {
      return {
        ...state,
        selectedId: action.contactId,
        message: '' // Clears the input
      };

    Це пов'язано з тим, що ви не хочете ділитися чернеткою повідомлення між кількома одержувачами. Але було б краще, якби ваша програма "запам'ятовувала" чернетки для кожного контакту окремо, відновлюючи їх при перемиканні контактів.

    Ваше завдання - змінити спосіб структурування стану так, щоб запам'ятовувати окрему чернетку повідомлення для кожного контакту. Вам потрібно буде внести деякі зміни до редуктора, початкового стану та компонентів.

    Ви можете структурувати свій стан таким чином:

    export const initialState = {
      selectedId: 0,
      messages: {
        0: 'Hello, Taylor', // Draft for contactId = 0
        1: 'Hello, Alice', // Draft for contactId = 1
      },
    };

    Синтаксис [key]: value обчисленої властивості може допомогти вам оновити об'єкт messages:

    {
      ...state.messages,
      [id]: message
    }
    import { useReducer } from 'react';
    import Chat from './Chat.js';
    import ContactList from './ContactList.js';
    import { initialState, messengerReducer } from './messengerReducer';
    
    export default function Messenger() {
      const [state, dispatch] = useReducer(messengerReducer, initialState);
      const message = state.message;
      const contact = contacts.find((c) => c.id === state.selectedId);
      return (
        <div>
          <ContactList
            contacts={contacts}
            selectedId={state.selectedId}
            dispatch={dispatch}
          />
          <Chat
            key={contact.id}
            message={message}
            contact={contact}
            dispatch={dispatch}
          />
        </div>
      );
    }
    
    const contacts = [
      {id: 0, name: 'Taylor', email: '[email protected]'},
      {id: 1, name: 'Alice', email: '[email protected]'},
      {id: 2, name: 'Bob', email: '[email protected]'},
    ];
    export const initialState = {
      selectedId: 0,
      message: 'Hello',
    };
    
    export function messengerReducer(state, action) {
      switch (action.type) {
        case 'changed_selection': {
          return {
            ...state,
            selectedId: action.contactId,
            message: '',
          };
        }
        case 'edited_message': {
          return {
            ...state,
            message: action.message,
          };
        }
        case 'sent_message': {
          return {
            ...state,
            message: '',
          };
        }
        default: {
          throw Error('Unknown action: ' + action.type);
        }
      }
    }
    export default function ContactList({contacts, selectedId, dispatch}) {
      return (
        <section className="contact-list">
          <ul>
            {contacts.map((contact) => (
              <li key={contact.id}>
                <button
                  onClick={() => {
                    dispatch({
                      type: 'changed_selection',
                      contactId: contact.id,
                    });
                  }}>
                  {selectedId === contact.id ? <b>{contact.name}</b> : contact.name}
                </button>
              </li>
            ))}
          </ul>
        </section>
      );
    }
    import { useState } from 'react';
    
    export default function Chat({contact, message, dispatch}) {
      return (
        <section className="chat">
          <textarea
            value={message}
            placeholder={'Chat to ' + contact.name}
            onChange={(e) => {
              dispatch({
                type: 'edited_message',
                message: e.target.value,
              });
            }}
          />
          <br />
          <button
            onClick={() => {
              alert(`Sending "${message}" to ${contact.email}`);
              dispatch({
                type: 'sent_message',
              });
            }}>
            Send to {contact.email}
          </button>
        </section>
      );
    }
    .chat,
    .contact-list {
      float: left;
      margin-bottom: 20px;
    }
    ul,
    li {
      list-style: none;
      margin: 0;
      padding: 0;
    }
    li button {
      width: 100px;
      padding: 10px;
      margin-right: 10px;
    }
    textarea {
      height: 150px;
    }

    Вам потрібно оновити редуктор, щоб зберігати та оновлювати окремі чернетки повідомлень для кожного контакту:

    // When the input is edited
    case 'edited_message': {
      return {
        // Keep other state like selection
        ...state,
        messages: {
          // Keep messages for other contacts
          ...state.messages,
          // But change the selected contact's message
          [state.selectedId]: action.message
        }
      };
    }

    Також слід оновити компонент Messenger, щоб прочитати повідомлення для поточного вибраного контакту:

    const message = state.messages[state.selectedId];

    Ось повне рішення:

    import { useReducer } from 'react';
    import Chat from './Chat.js';
    import ContactList from './ContactList.js';
    import { initialState, messengerReducer } from './messengerReducer';
    
    export default function Messenger() {
      const [state, dispatch] = useReducer(messengerReducer, initialState);
      const message = state.messages[state.selectedId];
      const contact = contacts.find((c) => c.id === state.selectedId);
      return (
        <div>
          <ContactList
            contacts={contacts}
            selectedId={state.selectedId}
            dispatch={dispatch}
          />
          <Chat
            key={contact.id}
            message={message}
            contact={contact}
            dispatch={dispatch}
          />
        </div>
      );
    }
    
    const contacts = [
      {id: 0, name: 'Taylor', email: '[email protected]'},
      {id: 1, name: 'Alice', email: '[email protected]'},
      {id: 2, name: 'Bob', email: '[email protected]'},
    ];
    export const initialState = {
      selectedId: 0,
      messages: {
        0: 'Hello, Taylor',
        1: 'Hello, Alice',
        2: 'Hello, Bob',
      },
    };
    
    export function messengerReducer(state, action) {
      switch (action.type) {
        case 'changed_selection': {
          return {
            ...state,
            selectedId: action.contactId,
          };
        }
        case 'edited_message': {
          return {
            ...state,
            messages: {
              ...state.messages,
              [state.selectedId]: action.message,
            },
          };
        }
        case 'sent_message': {
          return {
            ...state,
            messages: {
              ...state.messages,
              [state.selectedId]: '',
            },
          };
        }
        default: {
          throw Error('Unknown action: ' + action.type);
        }
      }
    }
    export default function ContactList({contacts, selectedId, dispatch}) {
      return (
        <section className="contact-list">
          <ul>
            {contacts.map((contact) => (
              <li key={contact.id}>
                <button
                  onClick={() => {
                    dispatch({
                      type: 'changed_selection',
                      contactId: contact.id,
                    });
                  }}>
                  {selectedId === contact.id ? <b>{contact.name}</b> : contact.name}
                </button>
              </li>
            ))}
          </ul>
        </section>
      );
    }
    import { useState } from 'react';
    
    export default function Chat({contact, message, dispatch}) {
      return (
        <section className="chat">
          <textarea
            value={message}
            placeholder={'Chat to ' + contact.name}
            onChange={(e) => {
              dispatch({
                type: 'edited_message',
                message: e.target.value,
              });
            }}
          />
          <br />
          <button
            onClick={() => {
              alert(`Sending "${message}" to ${contact.email}`);
              dispatch({
                type: 'sent_message',
              });
            }}>
            Send to {contact.email}
          </button>
        </section>
      );
    }
    .chat,
    .contact-list {
      float: left;
      margin-bottom: 20px;
    }
    ul,
    li {
      list-style: none;
      margin: 0;
      padding: 0;
    }
    li button {
      width: 100px;
      padding: 10px;
      margin-right: 10px;
    }
    textarea {
      height: 150px;
    }

    Цікаво, що вам не потрібно було змінювати жодного з обробників подій, щоб реалізувати цю відмінну поведінку. Без редуктора вам довелося б змінювати кожен обробник події, який оновлює стан.

    Реалізувати useReducer з нуля

    У попередніх прикладах ви імпортували хук useReducer з React. Цього разу ви реалізуєте сам хук useReducer! Ось заглушка для початку. Це не повинно займати більше 10 рядків коду.

    Щоб перевірити ваші зміни, спробуйте ввести дані або вибрати контакт.

    Ось детальніший ескіз реалізації:

    export function useReducer(reducer, initialState) {
      const [state, setState] = useState(initialState);
    
      function dispatch(action) {
        // ???
      }
    
      return [state, dispatch];
    }

    Пам'ятайте, що функція-редуктор приймає два аргументи - поточний стан та об'єкт дії - і повертає наступний стан. Що з цим має робити ваша реалізація диспетчера?

    import { useReducer } from './MyReact.js';
    import Chat from './Chat.js';
    import ContactList from './ContactList.js';
    import { initialState, messengerReducer } from './messengerReducer';
    
    export default function Messenger() {
      const [state, dispatch] = useReducer(messengerReducer, initialState);
      const message = state.messages[state.selectedId];
      const contact = contacts.find((c) => c.id === state.selectedId);
      return (
        <div>
          <ContactList
            contacts={contacts}
            selectedId={state.selectedId}
            dispatch={dispatch}
          />
          <Chat
            key={contact.id}
            message={message}
            contact={contact}
            dispatch={dispatch}
          />
        </div>
      );
    }
    
    const contacts = [
      {id: 0, name: 'Taylor', email: '[email protected]'},
      {id: 1, name: 'Alice', email: '[email protected]'},
      {id: 2, name: 'Bob', email: '[email protected]'},
    ];
    export const initialState = {
      selectedId: 0,
      messages: {
        0: 'Hello, Taylor',
        1: 'Hello, Alice',
        2: 'Hello, Bob',
      },
    };
    
    export function messengerReducer(state, action) {
      switch (action.type) {
        case 'changed_selection': {
          return {
            ...state,
            selectedId: action.contactId,
          };
        }
        case 'edited_message': {
          return {
            ...state,
            messages: {
              ...state.messages,
              [state.selectedId]: action.message,
            },
          };
        }
        case 'sent_message': {
          return {
            ...state,
            messages: {
              ...state.messages,
              [state.selectedId]: '',
            },
          };
        }
        default: {
          throw Error('Unknown action: ' + action.type);
        }
      }
    }
    import { useState } from 'react';
    
    export function useReducer(reducer, initialState) {
      const [state, setState] = useState(initialState);
    
      // ???
    
      return [state, dispatch];
    }
    export default function ContactList({contacts, selectedId, dispatch}) {
      return (
        <section className="contact-list">
          <ul>
            {contacts.map((contact) => (
              <li key={contact.id}>
                <button
                  onClick={() => {
                    dispatch({
                      type: 'changed_selection',
                      contactId: contact.id,
                    });
                  }}>
                  {selectedId === contact.id ? <b>{contact.name}</b> : contact.name}
                </button>
              </li>
            ))}
          </ul>
        </section>
      );
    }
    import { useState } from 'react';
    
    export default function Chat({contact, message, dispatch}) {
      return (
        <section className="chat">
          <textarea
            value={message}
            placeholder={'Chat to ' + contact.name}
            onChange={(e) => {
              dispatch({
                type: 'edited_message',
                message: e.target.value,
              });
            }}
          />
          <br />
          <button
            onClick={() => {
              alert(`Sending "${message}" to ${contact.email}`);
              dispatch({
                type: 'sent_message',
              });
            }}>
            Send to {contact.email}
          </button>
        </section>
      );
    }
    .chat,
    .contact-list {
      float: left;
      margin-bottom: 20px;
    }
    ul,
    li {
      list-style: none;
      margin: 0;
      padding: 0;
    }
    li button {
      width: 100px;
      padding: 10px;
      margin-right: 10px;
    }
    textarea {
      height: 150px;
    }

    Диспетчеризація дії викликає редуктор з поточним станом та дією і зберігає результат як наступний стан. Ось як це виглядає у коді:

    import { useReducer } from './MyReact.js';
    import Chat from './Chat.js';
    import ContactList from './ContactList.js';
    import { initialState, messengerReducer } from './messengerReducer';
    
    export default function Messenger() {
      const [state, dispatch] = useReducer(messengerReducer, initialState);
      const message = state.messages[state.selectedId];
      const contact = contacts.find((c) => c.id === state.selectedId);
      return (
        <div>
          <ContactList
            contacts={contacts}
            selectedId={state.selectedId}
            dispatch={dispatch}
          />
          <Chat
            key={contact.id}
            message={message}
            contact={contact}
            dispatch={dispatch}
          />
        </div>
      );
    }
    
    const contacts = [
      {id: 0, name: 'Taylor', email: '[email protected]'},
      {id: 1, name: 'Alice', email: '[email protected]'},
      {id: 2, name: 'Bob', email: '[email protected]'},
    ];
    export const initialState = {
      selectedId: 0,
      messages: {
        0: 'Hello, Taylor',
        1: 'Hello, Alice',
        2: 'Hello, Bob',
      },
    };
    
    export function messengerReducer(state, action) {
      switch (action.type) {
        case 'changed_selection': {
          return {
            ...state,
            selectedId: action.contactId,
          };
        }
        case 'edited_message': {
          return {
            ...state,
            messages: {
              ...state.messages,
              [state.selectedId]: action.message,
            },
          };
        }
        case 'sent_message': {
          return {
            ...state,
            messages: {
              ...state.messages,
              [state.selectedId]: '',
            },
          };
        }
        default: {
          throw Error('Unknown action: ' + action.type);
        }
      }
    }
    import { useState } from 'react';
    
    export function useReducer(reducer, initialState) {
      const [state, setState] = useState(initialState);
    
      function dispatch(action) {
        const nextState = reducer(state, action);
        setState(nextState);
      }
    
      return [state, dispatch];
    }
    export default function ContactList({contacts, selectedId, dispatch}) {
      return (
        <section className="contact-list">
          <ul>
            {contacts.map((contact) => (
              <li key={contact.id}>
                <button
                  onClick={() => {
                    dispatch({
                      type: 'changed_selection',
                      contactId: contact.id,
                    });
                  }}>
                  {selectedId === contact.id ? <b>{contact.name}</b> : contact.name}
                </button>
              </li>
            ))}
          </ul>
        </section>
      );
    }
    import { useState } from 'react';
    
    export default function Chat({contact, message, dispatch}) {
      return (
        <section className="chat">
          <textarea
            value={message}
            placeholder={'Chat to ' + contact.name}
            onChange={(e) => {
              dispatch({
                type: 'edited_message',
                message: e.target.value,
              });
            }}
          />
          <br />
          <button
            onClick={() => {
              alert(`Sending "${message}" to ${contact.email}`);
              dispatch({
                type: 'sent_message',
              });
            }}>
            Send to {contact.email}
          </button>
        </section>
      );
    }
    .chat,
    .contact-list {
      float: left;
      margin-bottom: 20px;
    }
    ul,
    li {
      list-style: none;
      margin: 0;
      padding: 0;
    }
    li button {
      width: 100px;
      padding: 10px;
      margin-right: 10px;
    }
    textarea {
      height: 150px;
    }

    Хоча у більшості випадків це не має значення, дещо точніша реалізація виглядає так:

    function dispatch(action) {
      setState((s) => reducer(s, action));
    }

    Це пов'язано з тим, що відправлені дії ставляться у чергу до наступного рендерингу, подібно до функцій оновлення.