useReducer

useReducer - це хук React, який дозволяє додати редуктор до вашого компонента.

const [state, dispatch] = useReducer(reducer, initialArg, init?)

Довдіник

useReducer(reducer, initialArg, init?)

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

import { useReducer } from 'react';

function reducer(state, action) {
  // ...
}

function MyComponent() {
  const [state, dispatch] = useReducer(reducer, { age: 42 });
  // ...

Дивіться більше прикладів нижче.

Параметри

  • reducer: Функція-редуктор, яка визначає спосіб оновлення стану. Вона повинна бути чистою, приймати стан і дію як аргументи і повертати наступний стан. Стан і дія можуть бути будь-яких типів.
  • .
  • initialArg: Значення, з якого обчислюється початковий стан. Це може бути значення будь-якого типу. Як з нього обчислюватиметься початковий стан, залежить від наступного аргументу init.
  • опціонально init: Функція ініціалізатора, яка повинна повернути початковий стан. Якщо її не вказано, початковий стан буде встановлено у initialArg. В іншому випадку, початковий стан встановлюється до результату виклику init(initialArg).
  • .

Повернення

useReducer повертає масив, що містить рівно два елементи:

  1. Поточний стан. Під час першого рендерингу встановлюється як init(initialArg) або initialArg (якщо немає init).
  2. Функція dispatch, яка дозволяє оновити стан до іншого значення і запустити повторний рендеринг.

Застереження


функція відправки функція

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

const [state, dispatch] = useReducer(reducer, { age: 42 });

function handleClick() {
  dispatch({ type: 'incremented_age' });
  // ...

React встановить наступний стан на результат виклику функції reducer, яку ви надали з поточним станом та дією, яку ви передали в dispatch.

Параметри

  • дія: Дія, яку виконує користувач. Це може бути значення будь-якого типу. Зазвичай, дія - це об'єкт з властивістю type, що ідентифікує його, і, за бажанням, іншими властивостями з додатковою інформацією.

Повернення

функції dispatch не мають значення, що повертається.

Застереження

  • Функція dispatch лише оновлює змінну стану для наступного рендерингу . Якщо ви прочитаєте змінну стану після виклику функції dispatch, ви все одно отримаєте старе значення, яке було на екрані до вашого виклику.

  • Якщо нове значення, яке ви надали, ідентичне поточному стану, як визначено порівнянням Object.is, React пропустить повторний рендеринг компонента та його дочірніх елементів. Це оптимізація. React все ще може викликати ваш компонент перед ігноруванням результату, але це не повинно впливати на ваш код.

  • React пакетно оновлює стан. Він оновлює екран після запуску всіх обробників подій та виклику їхніх функцій set. Це запобігає багаторазовому повторному рендерингу під час однієї події. У рідкісних випадках, коли вам потрібно змусити React оновити екран раніше, наприклад, для доступу до DOM, ви можете використовувати flushSync.


Використання

Додавання редуктора до компонента

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

import { useReducer } from 'react';

function reducer(state, action) {
  // ...
}

function MyComponent() {
  const [state, dispatch] = useReducer(reducer, { age: 42 });
  // ...

useReducer повертає масив, що містить рівно два елементи:

  1. Поточний стан Крок коду</CodeStep> цієї змінної стану, початково встановленої у <CodeStep data-step="3">початковий стан</CodeStep> ви надали.</li> <li>Функція <CodeStep data-step="2"><code>dispatch що дозволяє змінити його на будь-яке інше значення у відповідь на взаємодію.

Щоб оновити те, що на екрані, викличте <code>dispatch з об'єктом, що представляє те, що зробив користувач, який називається дія:

function handleClick() {
  dispatch({ type: 'incremented_age' });
}

React передасть поточний стан і дію до вашої функції-редуктора </CodeStep> . Ваш редуктор обчислить і поверне наступний стан. React збереже цей наступний стан, відрендерить ваш компонент з ним і оновить інтерфейс користувача.</p> </MaxWidth> <Пісочний пакет> <pre><code class="language-js">import { useReducer } from 'react'; функція reducer(state, action) { if (action.type === 'incremented_age') { return { age: state.age + 1 }; } згенерує помилку ('Невідома дія.'); } експортувати функцію за замовчуванням Counter() { const [state, dispatch] = useReducer(reducer, { age: 42 }); return ( <> <button onClick={() => { dispatch({ type: 'incremented_age' }) }}> Збільшення віку </button> <p>Привіт! Ви - {state.age}.</p> </> ); }

button { display: block; margin-top: 10px; }

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


Запис функції редуктора

Функція-редуктор оголошується так:

function reducer(state, action) {
  // ...
}

Далі потрібно заповнити код, який буде обчислювати і повертати наступний стан. За домовленістю його прийнято записувати як інструкцію switch. Для кожного case у switch обчислити і повернути деякий наступний стан.

function reducer(state, action) {
  switch (action.type) {
    case 'incremented_age': {
      return {
        name: state.name,
        age: state.age + 1
      };
    }
    case 'changed_name': {
      return {
        name: action.nextName,
        age: state.age
      };
    }
  }
  throw Error('Unknown action: ' + action.type);
}

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

function Form() {
  const [state, dispatch] = useReducer(reducer, { name: 'Taylor', age: 42 });
  
  function handleButtonClick() {
    dispatch({ type: 'incremented_age' });
  }

  function handleInputChange(e) {
    dispatch({
      type: 'changed_name',
      nextName: e.target.value
    });
  }
  // ...

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

Прочитайте Вилучення логіки станів у редуктор, щоб дізнатися більше.

Стан доступний лише для читання. Не змінюйте жодних об'єктів чи масивів у стані:

function reducer(state, action) {
  switch (action.type) {
    case 'incremented_age': {
      // 🚩 Don't mutate an object in state like this:
      state.age = state.age + 1;
      return state;
    }

Натомість завжди повертайте нові об'єкти з вашого редуктора:

function reducer(state, action) {
  switch (action.type) {
    case 'incremented_age': {
      // ✅ Instead, return a new object
      return {
        ...state,
        age: state.age + 1
      };
    }

Прочитайте Оновлення об'єктів у стані та Оновлення масивів у стані, щоб дізнатися більше.

Форма (об'єкт)

У цьому прикладі редуктор керує об'єктом стану з двома полями: name та age.

import { useReducer } from 'react';

function reducer(state, action) {
  switch (action.type) {
    case 'incremented_age': {
      return {
        name: state.name,
        age: state.age + 1
      };
    }
    case 'changed_name': {
      return {
        name: action.nextName,
        age: state.age
      };
    }
  }
  throw Error('Unknown action: ' + action.type);
}

const initialState = { name: 'Taylor', age: 42 };

export default function Form() {
  const [state, dispatch] = useReducer(reducer, initialState);

  function handleButtonClick() {
    dispatch({ type: 'incremented_age' });
  }

  function handleInputChange(e) {
    dispatch({
      type: 'changed_name',
      nextName: e.target.value
    }); 
  }

  return (
    <>
      <input
        value={state.name}
        onChange={handleInputChange}
      />
      <button onClick={handleButtonClick}>
        Increment age
      </button>
      <p>Hello, {state.name}. You are {state.age}.</p>
    </>
  );
}
button { display: block; margin-top: 10px; }

Список (масив)

У цьому прикладі редуктор керує масивом завдань. Масив потрібно оновити без мутації.

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

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

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

Запис стислої логіки оновлення за допомогою Immer

Якщо оновлення масивів та об'єктів без мутації здається вам нудним, ви можете скористатися бібліотекою на кшталт Immer для зменшення повторюваного коду. Immer дозволяє писати стислий код так, ніби ви мутуєте об'єкти, але за лаштунками вона виконує незмінні оновлення:

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"
  }
}

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

React зберігає початковий стан один раз і ігнорує його при наступних рендерах.

function createInitialState(username) {
  // ...
}

function TodoList({ username }) {
  const [state, dispatch] = useReducer(reducer, createInitialState(username));
  // ...

Хоча результат createInitialState(username) використовується лише для початкового рендерингу, ви все одно викликаєте цю функцію для кожному рендерингу. Це може бути марнотратством, якщо створюються великі масиви або виконуються складні обчислення.

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

function createInitialState(username) {
  // ...
}

function TodoList({ username }) {
  const [state, dispatch] = useReducer(reducer, username, createInitialState);
  // ...

Зверніть увагу, що ви передаєте createInitialState, яка є самою функцією, а не createInitialState(), яка є результатом її виклику. Таким чином, початковий стан не створюється повторно після ініціалізації.

У наведеному вище прикладі createInitialState отримує аргумент ім'я користувача. Якщо вашому ініціалізатору не потрібна інформація для обчислення початкового стану, ви можете передати null як другий аргумент до useReducer.

Передача функції ініціалізатора

У цьому прикладі передається функція ініціалізації, тому функція createInitialState виконується лише під час ініціалізації. Вона не виконується під час повторного рендерингу компонента, наприклад, коли ви вводите дані у вікно введення.

import TodoList from './TodoList.js';

export default function App() {
  return <TodoList username="Taylor" />;
}
import { useReducer } from 'react';

function createInitialState(username) {
  const initialTodos = [];
  for (let i = 0; i < 50; i++) {
    initialTodos.push({
      id: i,
      text: username + "'s task #" + (i + 1)
    });
  }
  return {
    draft: '',
    todos: initialTodos,
  };
}

function reducer(state, action) {
  switch (action.type) {
    case 'changed_draft': {
      return {
        draft: action.nextDraft,
        todos: state.todos,
      };
    };
    case 'added_todo': {
      return {
        draft: '',
        todos: [{
          id: state.todos.length,
          text: state.draft
        }, ...state.todos]
      }
    }
  }
  throw Error('Unknown action: ' + action.type);
}

export default function TodoList({ username }) {
  const [state, dispatch] = useReducer(
    reducer,
    username,
    createInitialState
  );
  return (
    <>
      <input
        value={state.draft}
        onChange={e => {
          dispatch({
            type: 'changed_draft',
            nextDraft: e.target.value
          })
        }}
      />
      <button onClick={() => {
        dispatch({ type: 'added_todo' });
      }}>Add</button>
      <ul>
        {state.todos.map(item => (
          <li key={item.id}>
            {item.text}
          </li>
        ))}
      </ul>
    </>
  );
}

Перехід до початкового стану безпосередньо

У цьому прикладі не передається функція ініціалізатора, тому функція createInitialState виконується під час кожного рендерингу, наприклад, коли ви вводите дані. Помітної різниці у поведінці не спостерігається, але такий код менш ефективний.

import TodoList from './TodoList.js';

export default function App() {
  return <TodoList username="Taylor" />;
}
import { useReducer } from 'react';

function createInitialState(username) {
  const initialTodos = [];
  for (let i = 0; i < 50; i++) {
    initialTodos.push({
      id: i,
      text: username + "'s task #" + (i + 1)
    });
  }
  return {
    draft: '',
    todos: initialTodos,
  };
}

function reducer(state, action) {
  switch (action.type) {
    case 'changed_draft': {
      return {
        draft: action.nextDraft,
        todos: state.todos,
      };
    };
    case 'added_todo': {
      return {
        draft: '',
        todos: [{
          id: state.todos.length,
          text: state.draft
        }, ...state.todos]
      }
    }
  }
  throw Error('Unknown action: ' + action.type);
}

export default function TodoList({ username }) {
  const [state, dispatch] = useReducer(
    reducer,
    createInitialState(username)
  );
  return (
    <>
      <input
        value={state.draft}
        onChange={e => {
          dispatch({
            type: 'changed_draft',
            nextDraft: e.target.value
          })
        }}
      />
      <button onClick={() => {
        dispatch({ type: 'added_todo' });
      }}>Add</button>
      <ul>
        {state.todos.map(item => (
          <li key={item.id}>
            {item.text}
          </li>
        ))}
      </ul>
    </>
  );
}

Налагодження

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

Виклик функції dispatchне змінює стан у коді, що виконується:

function handleClick() {
  console.log(state.age);  // 42

  dispatch({ type: 'incremented_age' }); // Request a re-render with 43
  console.log(state.age);  // Still 42!

  setTimeout(() => {
    console.log(state.age); // Also 42!
  }, 5000);
}

Це тому, що стани поводяться як знімок. Оновлення стану запитує інший рендеринг з новим значенням стану, але не впливає на змінну state JavaScript у вашому вже запущеному обробнику події.

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

const action = { type: 'incremented_age' };
dispatch(action);

const nextState = reducer(state, action);
console.log(state);     // { age: 42 }
console.log(nextState); // { age: 43 }

Я відправив дію, але екран не оновлюється

React проігнорує ваше оновлення, якщо наступний стан дорівнює попередньому, як визначено порівнянням Object.is. Зазвичай це трапляється, коли ви безпосередньо змінюєте об'єкт або масив у стані:

function reducer(state, action) {
  switch (action.type) {
    case 'incremented_age': {
      // 🚩 Wrong: mutating existing object
      state.age++;
      return state;
    }
    case 'changed_name': {
      // 🚩 Wrong: mutating existing object
      state.name = action.nextName;
      return state;
    }
    // ...
  }
}

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

function reducer(state, action) {
  switch (action.type) {
    case 'incremented_age': {
      // ✅ Correct: creating a new object
      return {
        ...state,
        age: state.age + 1
      };
    }
    case 'changed_name': {
      // ✅ Correct: creating a new object
      return {
        ...state,
        name: action.nextName
      };
    }
    // ...
  }
}

Частина стану мого редуктора стає невизначеною після диспетчеризації

Переконайтеся, що кожна гілка case гілки копіює всі існуючі поля при поверненні нового стану:

function reducer(state, action) {
  switch (action.type) {
    case 'incremented_age': {
      return {
        ...state, // Don't forget this!
        age: state.age + 1
      };
    }
    // ...

Без ...state вище, повернутий наступний стан містив би лише поле age і нічого більше.


Весь стан мого редуктора стає невизначеним після відправлення

Якщо ваш стан несподівано стає undefined, ймовірно, ви забули повернути стан в одному з випадків, або ваш тип дії не відповідає жодному з операторів випадку. Щоб з'ясувати причину, виведіть помилку за межі switch:

function reducer(state, action) {
  switch (action.type) {
    case 'incremented_age': {
      // ...
    }
    case 'edited_name': {
      // ...
    }
  }
  throw Error('Unknown action: ' + action.type);
}

Ви також можете використовувати статичну перевірку типів, таку як TypeScript, для виявлення таких помилок.


Я отримую помилку: "Занадто багато повторних рендерингів"

Ви можете отримати помилку з повідомленням: Too many re-renders. React limits the number of renders to prevent an infinite loop. Зазвичай це означає, що ви безумовно відправляєте дію під час рендеру, тому ваш компонент потрапляє у цикл: рендер, відправка стану (яка викликає рендер), рендер, відправка стану (яка викликає рендер) і так далі. Дуже часто це спричинено помилкою у визначенні обробника події:

// 🚩 Wrong: calls the handler during render
return <button onClick={handleClick()}>Click me</button>

// ✅ Correct: passes down the event handler
return <button onClick={handleClick}>Click me</button>

// ✅ Correct: passes down an inline function
return <button onClick={(e) => handleClick(e)}>Click me</button>

Якщо ви не можете знайти причину цієї помилки, натисніть на стрілку поруч з помилкою в консолі та перегляньте стек JavaScript, щоб знайти конкретний виклик функції dispatch, відповідальний за помилку.


Моя функція редуктора або ініціалізатора запускається двічі

У Суворому режимі, React викличе ваші функції редуктора та ініціалізатора двічі. Це не повинно зламати ваш код.

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

Наприклад, ця функція нечистого редуктора мутує масив у стані:

function reducer(state, action) {
  switch (action.type) {
    case 'added_todo': {
      // 🚩 Mistake: mutating state
      state.todos.push({ id: nextId++, text: action.text });
      return state;
    }
    // ...
  }
}

Оскільки React викликає вашу функцію-редуктор двічі, ви побачите, що todo було додано двічі, і зрозумієте, що сталася помилка. У цьому прикладі ви можете виправити помилку шляхом заміни масиву замість його мутації:

function reducer(state, action) {
  switch (action.type) {
    case 'added_todo': {
      // ✅ Correct: replacing with new state
      return {
        ...state,
        todos: [
          ...state.todos,
          { id: nextId++, text: action.text }
        ]
      };
    }
    // ...
  }
}

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

Прочитайте Збереження компонентів у чистоті щоб дізнатися більше.