Хук useState в React: Полное руководство

Источник: «useState in React: A complete guide»
В React хук useState позволяет добавлять состояние в функциональные компоненты. useState возвращает массив с двумя значениями: текущее состояние и функцию для его обновления.

Хук принимает в качестве аргумента начальное значение состояния и возвращает обновлённое значение состояния каждый раз, при вызове функции setter. Его можно использовать следующим образом:

const [state, setState] = useState(initialValue);

Здесь initialValue — значение, с которого вы хотите начать, а state — текущее значение состояния, которое может быть использовано в вашем компоненте. Функция setState может быть использована для обновления state, вызывая повторный рендеринг компонента.

Хук useState в React является эквивалентом this.state/this.setSate для функциональных компонентов.

Классы и функциональные компоненты React

В React есть два типа компонентов:

Как видите, здесь нет методов состояния или жизненного цикла. Однако начиная с 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>
);
};

Code Sandbox

Однако эта функция обновления не обновляет значение сразу. Вместо этого она ставит операцию обновления в очередь. Затем, после повторного рендеринга компонента, аргумент 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>
);
};

Code Sandbox

Реализация объекта в роли переменной состояния с хуком useState

При использовании объектов необходимо помнить о двух вещах, связанных с обновлениями:

Что касается первого пункта: если использовать то же значение, что и текущее состояние, для обновления состояния (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>
);
};

Code Sandbox.

Вместо того чтобы создавать новый объект, в приведённом выше примере мутирует существующий объект состояния. Для 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 });
});
}}

Code Sandbox

Однако 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:

Второе правило легко выполнить. Не используйте 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:

  1. React инициализирует список хуков и переменную, отслеживающую текущий хук
  2. React вызывает компонент в первый раз
  3. React находит вызов useState, создаёт новый объект Hook (с начальным состоянием), изменяет текущую переменную Hook, чтобы она указывала на этот объект, добавляет объект в список Hooks и возвращает массив с начальным состоянием и функцией для его обновления
  4. React находит ещё один вызов useState и повторяет действия предыдущего шага, сохраняя новый объект Hook и изменяя текущую переменную Hook
  5. Состояние компонента изменяется
  6. React отправляет операцию обновления состояния (выполняемую функцией, возвращаемой функцией useState) в очередь для обработки
  7. React определяет, что нужно перерисовать компонент
  8. React сбрасывает текущую переменную Hook и вызывает компонент
  9. React находит вызов useState, но на этот раз, поскольку уже есть Hook на первой позиции списка Hooks, он просто изменяет текущую переменную Hook и возвращает массив с текущим состоянием и функцию для его обновления
  10. React находит ещё один вызов useState и, поскольку Hook существует во второй позиции, снова просто изменяет текущую переменную Hook и возвращает массив с текущим состоянием и функцией для его обновления

Если любите читать код, посмотрите класс ReactFiberHooks, чтобы узнать, как работают Hooks под капотом.

Хуки React useState vs. useEffect

useState и useEffect позволяют вам управлять состоянием и побочными эффектами в ваших функциональных компонентах. Однако они служат разным целям и должны использоваться по-разному:

Например, рассмотрим компонент, получающий данные из 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

Комментарии


Дополнительные материалы

Предыдущая Статья

Создание CLI-приложения с Laravel и Docker

Следующая Статья

Кэширование зависимостей в GitHub Action