Хук useState в React: Полное руководство
useState
позволяет добавлять состояние в функциональные компоненты. useState
возвращает массив с двумя значениями: текущее состояние и функцию для его обновления.Хук принимает в качестве аргумента начальное значение состояния и возвращает обновлённое значение состояния каждый раз, при вызове функции setter
. Его можно использовать следующим образом:
const [state, setState] = useState(initialValue);
Здесь initialValue
— значение, с которого вы хотите начать, а state
— текущее значение состояния, которое может быть использовано в вашем компоненте. Функция setState
может быть использована для обновления state
, вызывая повторный рендеринг компонента.
Хук useState
в React является эквивалентом this.state
/this.setSate
для функциональных компонентов.
Классы и функциональные компоненты React
В React есть два типа компонентов:
Компоненты класса: Классы ES6, расширяющие встроенные методы
Component
и методы жизненного цикла:import { Component } from 'react';
class Message extends Component {
constructor(props) {
super(props);
this.state = {
message: ''
};
}
componentDidMount() {
/* ... */
}
render() {
return <div>{this.state.message}</div>;
}
}Функциональные компоненты: Функции, принимающие аргументы в качестве свойств компонента и возвращающие валидный JSX, как показано ниже:
function Message(props) {
return <div>{props.message}</div>
}
// Или в виде стрелочной функции
const Message = (props) => <div>{props.message}</div>
Как видите, здесь нет методов состояния или жизненного цикла. Однако начиная с React v16.8, можно использовать хуки. Хуки React, начинающиеся со слова "use
", — это функции, добавляющие переменные состояния в функциональные компоненты и использующие методы жизненного цикла классов.
Что делает UseState
useState
позволяет добавлять состояние в функциональные компоненты. Вызов React.useState
внутри функционального компонента генерирует один фрагмент состояния, связанный с этим компонентом.
Если в классе состояние — это всегда объект, то в Хуках состояние может быть любого типа. Каждый элемент состояния содержит одно значение: object
, array
, Boolean
или любой другой тип, который вы можете себе представить.
Итак, когда следует использовать хук useState
? Он удобен для управления состоянием локальных компонентов, но для больших проектов могут потребоваться дополнительные решения по управлению состоянием.
Что может хранить UseState
В React useState
может хранить значение любого типа, в то время как состояние в компоненте класса ограничено объектом. Это включает в себя примитивные типы данных, такие как string
, number
и Boolean
, а также сложные типы данных, такие как array
, object
и function
. Он может охватывать даже пользовательские типы данных, такие как экземпляры классов.
В принципе, всё, что может быть сохранено в переменной JavaScript, может быть сохранено в состоянии, управляемом useState
.
Обновление объектов и массивов в useState
Никогда не изменяйте напрямую объект или массив, хранящийся в useState
. Вместо этого следует создать новую, обновлённую версию объекта или массива и вызвать setState
с новой версией:
// Объекты
const [state, setState] = useState({ name: 'John', age: 30 });
const updateName = () => {
setState({ ...state, name: 'Jane' });
};
const updateAge = () => {
setState({ ...state, age: state.age + 1 });
};
// Массивы
const [array, setArray] = useState([1, 2, 3, 4, 5]);
const addItem = () => {
setArray([...array, 6]);
};
const removeItem = () => {
setArray(array.slice(0, array.length - 1));
};
Объявление состояния в React
useState
— это именованный экспорт из react
. Чтобы использовать его, вы можете писать React.useState
или импортировав его, писать useState
:
import React, { useState } from 'react';
Объект state
может быть объявлен в классе и позволяет объявить более одной переменной состояния, как показано ниже:
import React from 'react';
class Message extends React.Component {
constructor(props) {
super(props);
this.state = {
message: '',
list: [],
};
}
/* ... */
}
Однако в отличие от объекта state
, хук useState
позволяет объявлять только одну переменную состояния (любого типа) за раз, например, так:
import React, { useState } from 'react';
const Message= () => {
const messageState = useState( '' );
const listState = useState( [] );
}
Функция useState
принимает начальное значение переменной состояния в качестве аргумента, и его можно передавать напрямую, как показано в предыдущем примере. Также можно использовать функцию для ленивой инициализации переменной. Это удобно, когда начальное состояние является результатом сложных вычислений:
const Message= () => {
const messageState = useState( () => expensiveComputation() );
/* ... */
}
Начальное значение будет присвоено только при первом рендере. Если это функция, то она будет выполнена только при первом рендере. При последующих рендерах (из-за изменения состояния компонента или родительского компонента) аргумент хука useState
будет игнорироваться, и будет извлекаться текущее значение.
Важно отметить, что если вы хотите обновлять состояние на основе новых свойств, которые получает компонент, использование только useState
не сработает. Это связано с тем, что useState
использует свой начальный аргумент только в первый раз — не каждый раз, когда свойство меняется. Посмотрите здесь, как правильно это сделать. Приведу пример этого способа:
const Message= (props) => {
const messageState = useState( props.message );
/* ... */
}
Но useState
возвращает не просто переменную, как следует из предыдущих примеров. Он возвращает массив, где первым элементом является переменная состояния, а вторым — функция для обновления значения переменной:
const Message= () => {
const messageState = useState( '' );
const message = messageState[0]; // Содержит ''
const setMessage = messageState[1]; // Это функция
}
Обычно для упрощения кода, показанного выше, используется деструктуризация массива:
const Message= () => {
const [message, setMessage]= useState( '' );
}
Таким образом, можно использовать переменную состояния в функциональном компоненте, как и любую другую переменную:
const Message = () => {
const [message, setMessage] = useState( '' );
return (
<p>
<strong>{message}</strong>
</p>
);
};
Но почему useState
возвращает массив? Потому что, по сравнению с объектом, массив более гибок и удобен в использовании. Если бы метод возвращал объект с фиксированным набором свойств, вы бы не смогли присваивать ему собственные имена.
Вместо этого вам придётся сделать что-то вроде этого (при условии, что свойствами объекта являются state
и setState
):
// Без использования деструктуризации объектов
const messageState = useState( '' );
const message = messageState.state;
const setMessage = messageState
// Использование деструктуризации объектов
const { state: message, setState: setMessage } = useState( '' );
const { state: list, setState: setList } = useState( [] );
Использование хуков React для обновления состояния
Второй элемент, возвращаемый useState
, — это функция, принимающая новое значение для обновления переменной состояния. Вот пример, использующий поле text
для обновления переменной состояния при каждом изменении:
const Message = () => {
const [message, setMessage] = useState( '' );
return (
<div>
<input
type="text"
value={message}
placeholder="Enter a message"
onChange={e => setMessage(e.target.value)}
/>
<p>
<strong>{message}</strong>
</p>
</div>
);
};
Однако эта функция обновления не обновляет значение сразу. Вместо этого она ставит операцию обновления в очередь. Затем, после повторного рендеринга компонента, аргумент useState
будет проигнорирован, и функция вернёт самое последнее значение.
При обновлении состояния на основе его предыдущего значения необходимо передать в функцию setter
функцию, обновляющую состояние. Эта функция получает в качестве аргумента предыдущее значение состояния и возвращает новое значение состояния, как показано ниже:
const Message = () => {
const [message, setMessage] = useState( '' );
return (
<div>
<input
type="text"
value={message}
placeholder="Enter some letters"
onChange={e => {
const val = e.target.value;
setMessage(prev => prev + val)
} }
/>
<p>
<strong>{message}</strong>
</p>
</div>
);
};
Реализация объекта в роли переменной состояния с хуком useState
При использовании объектов необходимо помнить о двух вещах, связанных с обновлениями:
- Важность неизменяемости
- Тот факт, что сеттер, возвращаемый
useState
, не объединяет объекты, как это делаетsetState()
в компонентах класса
Что касается первого пункта: если использовать то же значение, что и текущее состояние, для обновления состояния (React использует Object.is()
для сравнения), React не будет вызывать повторный рендеринг.
При работе с объектами легко допустить следующую ошибку:
const Message = () => {
const [messageObj, setMessage] = useState({ message: '' });
return (
<div>
<input
type="text"
value={messageObj.message}
placeholder="Enter a message"
onChange={e => {
messageObj.message = e.target.value;
setMessage(messageObj); // Не работает
}}
/>
<p>
<strong>{messageObj.message}</strong>
</p>
</div>
);
};
Вместо того чтобы создавать новый объект, в приведённом выше примере мутирует существующий объект состояния. Для React это один и тот же объект. Чтобы это сработало, необходимо создать новый объект, как уже говорилось ранее:
onChange={e => {
const newMessageObj = { message: e.target.value };
setMessage(newMessageObj); // Теперь это работает
}}
Это приводит ко второму важному моменту, который необходимо запомнить: при обновлении переменной состояния, в отличие от this.setState
в компоненте класса, функция, возвращаемая useState
, не объединяет автоматически объекты обновления — она их заменяет.
Следуя предыдущему примеру, если добавить ещё одно свойство к объекту сообщения (id
), как показано ниже:
const Message = () => {
const [messageObj, setMessage] = useState({ message: '', id: 1 });
return (
<div>
<input
type="text"
value={messageObj.message}
placeholder="Enter a message"
onChange={e => {
const newMessageObj = { message: e.target.value };
setMessage(newMessageObj);
}}
/>
<p>
<strong>{messageObj.id} : {messageObj.message}</strong>
</p>
</div>
);
};
И мы обновляем только свойство message
, как в примере выше, React заменит исходный объект состояния { message: '', id: 1 }
на объект, используемый в событии onChange
, содержащий только свойство message
:
{ message: 'message entered' } // свойство id утеряно
Вы можете посмотреть, как теряется свойство id
в Code Sandbox.
Вы можете повторить поведение функции setState()
, используя аргумент функции, содержащий заменяемый объект и spread синтаксис объекта:
onChange={e => {
const val = e.target.value;
setMessage(prevState => {
return { ...prevState, message: val }
});
}}
Часть ...prevState
получит все свойства объекта, а часть message: val
перезапишет свойство message
. Это приведёт к тому же результату, что и использование Object.assign()
(только не забудьте создать новый объект):
onChange={e => {
const val = e.target.value;
setMessage(prevState => {
return Object.assign({}, prevState, { message: val });
});
}}
Однако spread синтаксис упрощает эту операцию, и он также работает с массивами. В принципе, при применении к массиву spread синтаксис удаляет скобки, и можно создать ещё один массив со значениями исходного массива:
[
...['a', 'b', 'c'],
'd'
]
// Это эквивалентно
[
'a', 'b', 'c',
'd'
]
Приведём пример, демонстрирующий, использование useState
с массивами:
const MessageList = () => {
const [message, setMessage] = useState("");
const [messageList, setMessageList] = useState([]);
return (
<div>
<input
type="text"
value={message}
placeholder="Enter a message"
onChange={e => {
setMessage(e.target.value);
}}
/>
<input
type="button"
value="Add"
onClick={e => {
setMessageList([
...messageList,
{
// Использует текущий размер в качестве ID (это необходимо для последующего итерации списка).
id: messageList.length + 1,
message: message
}
]);
setMessage(""); // Очистка текстового поля
}}
/>
<ul>
{messageList.map(m => (
<li key={m.id}>{m.message}</li>
))}
</ul>
</div>
);
};
Следует быть осторожным при применении spread синтаксиса к многомерным массивам, поскольку он выполняет только поверхностное копирование, то есть вложенные массивы не будут полностью скопированы и по-прежнему будут ссылаться на исходные данные.
Как обновить состояние во вложенном объекте в React с помощью хуков
В JavaScript многомерные массивы — это массивы внутри массивов, как показано ниже:
[
['value1','value2'],
['value3','value4']
]
Их можно использовать для группировки всех переменных состояния в одном месте. Однако для этой цели лучше использовать вложенные объекты, как здесь:
{
'row1' : {
'key1' : 'value1',
'key2' : 'value2'
},
'row2' : {
'key3' : 'value3',
'key4' : 'value4'
}
}
Но при работе с многомерными массивами и вложенными объектами возникает проблема: Object.assign
и spread синтаксис создают поверхностную копию, вместо глубокой.
Из документации по spread синтаксису:
Примечание: Spread syntax на самом деле переходит лишь на один уровень глубже при копировании массива. Таким образом, он может не подходить для копирования многоразмерных массивов, как показывает следующий пример: (так же как и c
Object.assign()
) и синтаксис spread
let a = [[1], [2], [3]];
let b = [...a];
b.shift().shift(); // 1
// Массив 'a' тоже затронут: [[], [2], [3]]
Данный вопрос на Stack Overflow содержит хорошие объяснения для приведённого выше примера, но важным моментом является то, что при использовании вложенных объектов нельзя просто использовать spread синтаксис для обновления объекта состояния. Например, рассмотрим следующий объект состояния:
const [messageObj, setMessage] = useState({
author: '',
message: {
id: 1,
text: ''
}
});
Следующие фрагменты кода демонстрируют несколько неверных способов обновления поля text
:
// Неверно
setMessage(prevState => ({
...prevState,
text: 'My message'
}));
// Неверно
setMessage(prevState => ({
...prevState.message,
text: 'My message'
}));
// Неверно
setMessage(prevState => ({
...prevState,
message: {
text: 'My message'
}
}));
Чтобы правильно обновить поле text
, необходимо создать новый объект, включающий все поля и вложенные объекты из исходного объекта:
// Верно
setMessage(prevState => ({
...prevState, // копируем все остальные поля/объекты
message: { // повторно создаём объект, содержащий обновляемое поле
...prevState.message, // копируем все поля объекта
text: 'My message' // перезаписываем значение обновляемого поля
}
}));
Таким же образом можно обновить поле author
объекта состояния:
// Верно
setMessage(prevState => ({
author: 'Joe', // перезаписываем значение обновляемого поля
...prevState.message // копируем все остальные поля/объекты
}));
Однако это при условии, что объект message
не меняется. Если он изменится, придётся обновлять объект таким способом:
// Correct
setMessage(prevState => ({
author: 'Joe', // обновляем значение поля
message: { // пересоздаём объект, содержащий обновляемое поле
...prevState.message, // копируем все поля объекта
text: 'My message' // перезаписываем значение обновляемого поля
}
}));
Управление состоянием React: Несколько переменных vs. Один объект состояния
При работе с несколькими полями или значениями в качестве состояния приложения есть возможность организовать состояние с помощью нескольких переменных состояния:
const [id, setId] = useState(-1);
const [message, setMessage] = useState('');
const [author, setAuthor] = useState('');
// Или переменная состояния объекта:
const [messageObj, setMessage] = useState({
id: 1,
message: '',
author: ''
});
Однако следует быть осторожным при использовании объектов состояния со сложной структурой (вложенные объекты). Рассмотрим пример:
const [messageObj, setMessage] = useState({
input: {
author: {
id: -1,
author: {
fName:'',
lName: ''
}
},
message: {
id: -1,
text: '',
date: now()
}
}
});
Если нужно обновить конкретное поле, вложенное глубоко в объект, придётся скопировать все остальные объекты вместе с парами ключ-значение объекта, содержащего это конкретное поле:
setMessage(prevState => ({
input: {
...prevState.input,
message: {
...prevState.input.message,
text: 'My message'
}
}
}));
В некоторых случаях клонирование глубоко вложенных объектов может быть затратным, поскольку React может перерисовывать части приложения, зависящие от полей, которые даже не изменились.
По этой причине первое, что вам необходимо рассмотреть, — это попытка сгладить объект(ы) состояния. В частности, документация React рекомендует разделить состояние на несколько переменных состояния, основываясь на том, какие значения имеют тенденцию изменяться вместе.
Если это невозможно, рекомендуется использовать библиотеки, помогающие работать с неизменяемыми объектами, например Immutable.js или Immer.
Правила использования UseState
UseState
подчиняется тем же правилам, что и все хуки React:
- Вызывайте хуки только на верхнем уровне
- Вызывайте хуки только из функций React
Второе правило легко выполнить. Не используйте useState
в компоненте класса:
class App extends React.Component {
render() {
const [message, setMessage] = useState( '' );
return (
<p>
<strong>{message}</strong>
</p>
);
}
}
Или обычной функции JavaScript (не вызываемой внутри функционального компонента):
function getState() {
const messageState = useState( '' );
return messageState;
}
const [message, setMessage] = getState();
const Message = () => {
/* ... */
}
Вы получите ошибку. Первое правило означает, что даже внутри функциональных компонентов не следует вызывать useState
в циклах, условиях или вложенных функциях, потому что React полагается на порядок вызова функций useState
, чтобы получить правильное значение для конкретной переменной состояния.
В связи с этим наиболее распространённой ошибкой является оборачивание вызовов useState
в условный оператор (они не будут выполняться постоянно):
if (condition) { // Иногда он будет выполняться, в результате чего порядок вызовов useState изменится
const [message, setMessage] = useState( '' );
setMessage( aMessage );
}
const [list, setList] = useState( [] );
setList( [1, 2, 3] );
Функциональный компонент может содержать множество вызовов useState
или других хуков. Каждый хук хранится в списке, и есть переменная, отслеживающая текущий выполненный хук.
Когда выполняется useState
, считывается состояние текущего хука (или инициализируется во время первого рендера), а затем переменная изменяется, указывая на следующий хук. Вот почему важно, чтобы вызовы хуков всегда происходили в одном и том же порядке. В противном случае может быть возвращено значение, принадлежащее другой переменной состояния.
В общих чертах, вот пошаговый пример, как React обрабатывает и отслеживает изменения состояния в функциональных компонентах при использовании хука useState
:
- React инициализирует список хуков и переменную, отслеживающую текущий хук
- React вызывает компонент в первый раз
- React находит вызов
useState
, создаёт новый объектHook
(с начальным состоянием), изменяет текущую переменнуюHook
, чтобы она указывала на этот объект, добавляет объект в списокHooks
и возвращает массив с начальным состоянием и функцией для его обновления - React находит ещё один вызов
useState
и повторяет действия предыдущего шага, сохраняя новый объектHook
и изменяя текущую переменнуюHook
- Состояние компонента изменяется
- React отправляет операцию обновления состояния (выполняемую функцией, возвращаемой функцией
useState
) в очередь для обработки - React определяет, что нужно перерисовать компонент
- React сбрасывает текущую переменную
Hook
и вызывает компонент - React находит вызов
useState
, но на этот раз, поскольку уже естьHook
на первой позиции спискаHooks
, он просто изменяет текущую переменнуюHook
и возвращает массив с текущим состоянием и функцию для его обновления - React находит ещё один вызов
useState
и, посколькуHook
существует во второй позиции, снова просто изменяет текущую переменнуюHook
и возвращает массив с текущим состоянием и функцией для его обновления
Если любите читать код, посмотрите класс ReactFiberHooks
, чтобы узнать, как работают Hooks
под капотом.
Хуки React useState
vs. useEffect
useState
и useEffect
позволяют вам управлять состоянием и побочными эффектами в ваших функциональных компонентах. Однако они служат разным целям и должны использоваться по-разному:
useState
- Позволяет добавить состояние в функциональный компонент.
- Возвращает массив с двумя значениями: текущее состояние и функцию сеттер для обновления состояния
- Используется для управления состоянием, которое должно быть обновлено и перерисовано на основе взаимодействия с пользователем или других событий в компоненте
useEffect
- Используется для управления побочными эффектами в функциональных компонентах. Побочный эффект — это любая операция, воздействующая на компонент за пределами его рендеринга, например, вызов API или установка таймера.
- Используется для управления побочными эффектами, запускаемыми после каждого рендера компонента или выполняющими очистку при размонтировании компонента
Например, рассмотрим компонент, получающий данные из API и отображающий их в виде списка:
const [data, setData] = useState([]);
useEffect(() => {
fetch('https://api.example.com/data')
.then(res => res.json())
.then(data => setData(data));
}, []);
return (
<ul>
{data.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
В этом примере хук useEffect
используется для вызова API и обновления состояния данных при каждом рендеринге компонента. Хук принимает в качестве аргумента функцию обратного вызова, выполняемую после каждого рендера компонента. Вторым аргументом useEffect
является массив зависимостей, определяющий, когда эффект должен быть запущен. В данном случае пустой массив означает, что эффект будет запущен только один раз при монтировании компонента.
Понимание хука useReducer
Для продвинутых случаев можно использовать хук useReducer
в качестве альтернативы useState
. Это удобно, если у вас сложная логика состояний, использующая несколько подзначений, или если состояние зависит от предыдущего.
Основные моменты, которые следует помнить о React хуке useState
- Функция обновления не сразу обновляет значение
- Если используется предыдущее значение для обновления состояния, необходимо передать функцию, получающую предыдущее значение и возвращающую обновлённое, например,
setMessage(previousVal => previousVal + currentVal)
. - Если используете то же значение, что и текущее состояние, для обновления состояния, React не будет вызывать повторный рендеринг
- В отличие от
this.setState
в компонентах класса,useState
не объединяет объекты при обновлении состояния, а заменяет их useState
подчиняется тем же правилам, что и все хуки. В частности, обратите внимание на порядок вызова этих функций (существует плагин ESLint, помогающий соблюдать эти правила)