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
повертає масив, що містить рівно два елементи:
- Поточний стан. Під час першого рендерингу встановлюється як
init(initialArg)
абоinitialArg
(якщо немаєinit
). - Функція
dispatch
, яка дозволяє оновити стан до іншого значення і запустити повторний рендеринг.
Застереження
useReducer
є хуком, тому ви можете викликати його лише на верхньому рівні вашого компонента або ваших власних хуків. Ви не можете викликати його всередині циклів або умов. Якщо вам це потрібно, витягніть новий компонент і перемістіть стан до нього.- У суворому режимі React двічі викличе ваш редуктор та ініціалізатордля того, щоб допомогти вам знайти випадкові домішки.Ця поведінка призначена лише для розробки і не впливає на виробництво. Якщо ваш редуктор та ініціалізатор чисті (як і має бути), це не повинно вплинути на вашу логіку. Результат одного з викликів ігнорується.
функція відправки
функція
Функція 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
повертає масив, що містить рівно два елементи:
- Поточний стан
Крок коду</CodeStep> цієї змінної стану, початково встановленої у <CodeStep data-step="3">початковий стан</CodeStep> ви надали.</li> <li>Функція <CodeStep data-step="2"><code>dispatch що дозволяє змінити його на будь-яке інше значення у відповідь на взаємодію.
Щоб оновити те, що на екрані, викличте
function handleClick() {
dispatch({ type: 'incremented_age' });
}
React передасть поточний стан і дію до вашої Функція-редуктор оголошується так: Далі потрібно заповнити код, який буде обчислювати і повертати наступний стан. За домовленістю його прийнято записувати як інструкцію Дії можуть мати будь-яку форму. Зазвичай прийнято передавати об'єкти з властивістю Назви типів дій є локальними для вашого компонента. Кожна дія описує одну взаємодію, навіть якщо це призводить до кількох змін у даних.Форма стану довільна, але зазвичай це буде об'єкт або масив. Прочитайте Вилучення логіки станів у редуктор, щоб дізнатися більше. Стан доступний лише для читання. Не змінюйте жодних об'єктів чи масивів у стані: Натомість завжди повертайте нові об'єкти з вашого редуктора: Прочитайте Оновлення об'єктів у стані та Оновлення масивів у стані, щоб дізнатися більше. У цьому прикладі редуктор керує об'єктом стану з двома полями: У цьому прикладі редуктор керує масивом завдань. Масив потрібно оновити без мутації. Якщо оновлення масивів та об'єктів без мутації здається вам нудним, ви можете скористатися бібліотекою на кшталт Immer для зменшення повторюваного коду. Immer дозволяє писати стислий код так, ніби ви мутуєте об'єкти, але за лаштунками вона виконує незмінні оновлення: React зберігає початковий стан один раз і ігнорує його при наступних рендерах. Хоча результат Щоб вирішити цю проблему, ви можете передати його як функцію-ініціалізатор функції до Зверніть увагу, що ви передаєте У наведеному вище прикладі У цьому прикладі передається функція ініціалізації, тому функція У цьому прикладі не передається функція ініціалізатора, тому функція Виклик Це тому, що стани поводяться як знімок. Оновлення стану запитує інший рендеринг з новим значенням стану, але не впливає на змінну Якщо вам потрібно вгадати наступне значення стану, ви можете обчислити його вручну, викликавши редуктор самостійно: React проігнорує ваше оновлення, якщо наступний стан дорівнює попередньому, як визначено порівнянням Ви мутували існуючий об'єкт Переконайтеся, що кожна гілка Без Якщо ваш стан несподівано стає Ви також можете використовувати статичну перевірку типів, таку як TypeScript, для виявлення таких помилок. Ви можете отримати помилку з повідомленням: Якщо ви не можете знайти причину цієї помилки, натисніть на стрілку поруч з помилкою в консолі та перегляньте стек JavaScript, щоб знайти конкретний виклик функції У Суворому режимі, React викличе ваші функції редуктора та ініціалізатора двічі. Це не повинно зламати ваш код. Ця поведінка лише для розробки допомагає вам зберігати компоненти чистими. React використовує результат одного з викликів, ігноруючи результат іншого. Поки ваш компонент, ініціалізатор та функції-зменшувачі є чистими, це не повинно впливати на вашу логіку. Однак, якщо вони випадково стануть нечистими, це допоможе вам помітити помилки. Наприклад, ця функція нечистого редуктора мутує масив у стані: Оскільки React викликає вашу функцію-редуктор двічі, ви побачите, що todo було додано двічі, і зрозумієте, що сталася помилка. У цьому прикладі ви можете виправити помилку шляхом заміни масиву замість його мутації: Тепер, коли ця функція-редуктор є чистою, її виклик зайвий раз не впливає на поведінку. Ось чому подвійний виклик цієї функції в React допомагає знаходити помилки. Чистими мають бути лише функції компонента, ініціалізатора та редуктора. Обробники подій не мають бути чистими, тому React ніколи не викликатиме ваші обробники подій двічі. Прочитайте Збереження компонентів у чистоті щоб дізнатися більше.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
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"
}
}
Уникнення відтворення початкового стану
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 }
Я відправив дію, але екран не оновлюється
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);
}
Я отримую помилку: "Занадто багато повторних рендерингів"
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>
dispatch
, відповідальний за помилку.
Моя функція редуктора або ініціалізатора запускається двічі
function reducer(state, action) {
switch (action.type) {
case 'added_todo': {
// 🚩 Mistake: mutating state
state.todos.push({ id: nextId++, text: action.text });
return state;
}
// ...
}
}
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 }
]
};
}
// ...
}
}