Статья поможет новичкам понять как работать с хуками, а также будет полезна и опытным разработчикам. Этой статьей открываю серию статей про хуки.
Использование хуков с одной стороны позволяет использовать методы жизненного цикла в функциональных компонентах и призваны улучшать производительность, что делает функциональные компоненты полноценным конкурентом классовых компонентов. С другой стороны, неправильное использование хуков приводит к лишним операциям и может свести на нет все преимущества функциональных компонентов.
В этой серии статей разберем основные хуки реакта и как их правильно использовать.
В серии статей поговорим про:
useState, как работать с состоянием компонента, что такое "бетчинг" (batching) и для чего в качестве аргумента можно передать функцию;
useEffect и как использовать cleanup и useLayoutEffect;
useMemo, useCallback и почему они напрямую касаются hoc memo. Разберем ситуации когда их нужно и не нужно использовать;
Context, useContext, когда использовать и улучшать производительность;
useRef, использование в качестве ссылки и безопасной переменной, forwardRef и useImperativeHandle;
useReducer как альтернатива useState и признаки, что пора его использовать. Также разберем нестандартных 2 случая использования useReducer;
В этой статье поговорим про:
Что из себя представляют хуки.
Базовое использование useState,
Асинхронность функции setState,
Что происходит когда новое состояние равно предыдущему,
В качестве начального состояния используем функцию,
Понятие хуков
Хук или по-русски крючок - это функция, которая вызывается в теле функционального компонента.
Как и любая функция, хук может принимать аргументы и возвращать значение.
Пример хука, который возвращает значение:
const [state, setState] = useState();
Пример хука, который принимает аргументы:
useEffect(() => {}, []);
Пример хука, который, и принимает аргументы, и возвращает значение:
const [state, setState] = useState(initialValue);
Реакт под капотом регистрирует все хуки, потому они должны находиться строго до любых условий (if, switch), и все хуки должны начинаться с префикса use: useState, useEffect, useMyCustomHook.
Базовое использование useState
Для работы с состоянием компонента используется useState.
const [state, setState] = useState<StateType>(initialValue);
// StateType - это тип состояния, можно использовать как примитивы:
// boolean, string, number
// так и объекты { test: number }, массивы Array<string>
// и вообще любые типы данных
Этот хук возвращает массив из двух элементов: состояния и функции изменения состояния [state, setState]
, принимает начальное состояние: useState(initialValue)
.
Внутри компонента используется так:
import React, { useState, FC } from "react";
export const ExampleFuncComponent: FC = () => {
const [state, setState] = useState<boolean>(true);
return (
<div>
<div>{state.toString()}</div>
<button onClick={() => setState(true)}> {/* 1 */}
установить true
</button>
<button onClick={() => setState(false)}> {/* 2 */}
установить false
</button>
<button onClick={() => setState(prevState => !prevState)}> {/* 3 */}
изменить состояние на противоположное
</button>
</div>
);
};
state
- это обычная переменная, с нем можно работать, как и с любой другой переменной: state.toString()
Может быть любым типом данных. Не пытайтесь изменять состояние вручную state = newState
, это нарушит работу вашего приложения. Единственный правильный путь - использовать функцию setState
.
setState
- это функция, которая принимает новое состояние (метка 1 и 2), либо функцию, которая принимает предыдущее состояние и возвращает новое состояние (метка 3). Главное - вызов функции запускает весь код функционального компонента повторно. В примере выше весь код, начиная с 3 по 16 строку будет вызван еще раз.
Кстати, как считаете можно ли использовать другие названия переменных, кроме state
и setState
? Например value
и setValue
?
const [state, setState] = useState<boolean>(true);
const [value, setValue] = useState<boolean>(true);
Даю 3 секунды подумать.
3
2
1
Названия может быть любыми, более того часто в одном компоненте используется несколько useState
и потому необходимо давать им разные названия.
Асинхронность setState
setState
- асинхронная функция. Под капотом реакт объединяет все мутации состояний, благодаря чему код функционального компонента будет вызван 1 раз, это называется "batching". Есть хорошая статья на эту тему.
import React, { useState, FC } from "react";
export const ExampleFuncComponent: FC = () => {
const [visible, setVisible] = useState(true);
const [count, setCount] = useState(0);
const onClick = () => {
setVisible((v) => !v);
setCount((v) => v + 1);
// уже обратили внимание,
// что необязательно называть переменную prevState?
};
console.log("update");
return (
<div>
<div>{visible.toString()}</div>
<button onClick={onClick}>
test
</button>
</div>
);
};
Консоль на строке 12 будет вызвана при монтировании компонента 1 раз и только 1 раз после нажатия на кнопку.
У этой особенности есть следствие. Как считаете, на сколько изменится счетчик при нажатии на кнопку test 0
и test 1
?
import React, { useState, FC } from "react";
export const ExampleFuncComponent: FC = () => {
const [count, setCount] = useState(0);
const onClick0 = () => {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
};
const onClick1 = () => {
setCount((v) => v + 1);
setCount((v) => v + 1);
setCount((v) => v + 1);
};
return (
<div>
<div>{count.toString()}</div>
<button onClick={onClick0}>
test 0
</button>
<button onClick={onClick1}>
test 1
</button>
</div>
);
};
При нажатии на test 1
счетчик увеличится на 3, а при нажатии на test 0
только на 1. Почему это происходит:
// Например count = 0
const onClick0 = () => {
// count + 1 = 0 + 1;
setCount(count + 1);
// Здесь можем ожидать, что count уже 1, но т.к. вызов setState асинхронный
// состояние еще не изменено, поэтому count по-прежнему 0
// count + 1 = 0 + 1;
setCount(count + 1);
// count + 1 = 0 + 1;
setCount(count + 1);
};
Поэтому если новое состояние опирается на предыдущее состояние, используйте функцию:
const onClick1 = () => {
setCount(v => v + 1);
setCount(v => v + 1);
setCount(v => v + 1);
};
Новое состояние равно предыдущему
Взгляните еще раз на этот код:
import React, { useState, FC } from "react";
export const ExampleFuncComponent: FC = () => {
const [state, setState] = useState<boolean>(true);
return (
<div>
<div>{state.toString()}</div>
<button onClick={() => setState(true)}>
установить true
</button>
<button onClick={() => setState(false)}>
установить false
</button>
<button onClick={() => setState(prevState => !prevState)}>
изменить состояние на противоположное
</button>
</div>
);
};
Нажмем кнопку установить false
3 раза, как думаете сколько раз обновится компонент?
Даю 3 секунды подумать:
3
2
1
Компонент обновится только 1 раз. Под капотом происходит сравнение предыдущего состояние и нового prevState === newState
, если результатом будет true
, компонент не будет обновляться.
Теперь вопрос, если новое состояние объект, будет обновляться компонент или нет? setState({});
3
2
1
Замените prevState === newState
на {} === {}
и станет очевидно, что обновление будет происходить, потому что объекты, массивы и функции - это ссылочные типы данных: даже когда они выглядят одинаково, они ссылаются на разные ячейки памяти, поэтому они не равны.
Если мы не хотим обновлять состояние когда объекты равны по содержимому, можно сравнить предыдущее состояние с текущим и если они равны, использовать предыдущее.
import React, { useState, FC } from "react";
export const SetSameLink: FC = () => {
const [state, setState] = useState({ test: "some" });
const mutate = (obj) => {
setState((prevState) => {
// Нужно проверить все свойства объектов, в нашем случае
// это свойство test
if (prevState.test === obj.test) return prevState;
return obj;
});
};
return (
<div>
<div>{JSON.stringify(state)}</div>
<button type="button" onClick={() => mutate({ test: "some" })}>
set state
</button>
</div>
);
};
Обратите внимание, если хотим чтобы не произошло обновления компонента, нужно вернуть предыдущее состояние return prevState
, а не его копию return { ...prevState }
.
Сравнивать по отдельности каждое свойство объектов неудобно, для этих целей рекомендую библиотеку fast-deep-equal.
Начальное значение - функция
В качестве начального значения useState может принимать не только само значение, но и функцию, которая вернет начальное значение.
Типы useState:
function useState<S>(initialState: S | (() => S)):
[S, Dispatch<SetStateAction<S>>];
Один из вариантов, когда хотим переиспользовать некоторую функцию, которая возвращает состояние. Например, когда берем начальное состояние из локального хранилища браузера:
const getStoredState = () => {
return localStorage.getItem('my-saved-state');
};
// Где-то в функциональном компоненте
const [state, setState] = useState(getStoredState);
// Где-то в другом месте
getStoredState();
Обратите внимание, необязательно в useState передавать результат вызова функции, достаточно передать функции и он сам ее вызовет: useState(getStoredState())
→ useState(getStoredState)
.
Заключение
Хук (по-русски крючок) - это функция. Как и любая функция, он может принимать аргументы и возвращать значение. Реакт регистрирует все хуки компонента, потому они должны быть до любых условий (if, switch) и начинаться с префикса use.
Хук useState возвращает массив из двух элементов (состояние, функция изменения состояние), а принимает начальное состояние.
Состояние - может быть чем угодно: строкой, числом, массивом и т.д. и с ним можно работать как с любой другой переменной, но изменять только с помощью функции изменения состояния.
Функция изменения состояния, принимает как новое значение setState(newState)
, так и функцию, которая получает предыдущее состояние, а результат ее вызова будет новым состоянием: setState(previosState => newState)
.
Функция изменения состояния - асинхронна, реакт объединяет несколько изменений состояния в один цикл обновления компонента. Потому любые счетчики setState(prevCount => prevCount + 1)
, переключатели setState(prevValue => !prevValue)
должны опираться на предыдущее состояние, иначе это может привести к непредсказуемым ошибкам.
Хук useState обновляет компонент только если новое состояние не равно предыдущему. Проверка осуществляется по строгому равенству prevState === newState
.
В качестве начального состояния можно передавать функцию, которая вернет начальное состояние useState(getStoredState)
. Это удобно, когда нам нужно переиспользовать эту функцию.
Если статья показалась полезной и интересной, ставьте палец вверх. Если есть вопросы - пишите в комментариях.
Также хочу пригласить всех желающих на бесплатный урок, который проведет мой коллега на платформе OTUS. В рамках урока вы узнаете для чего разработчику на React.js умение писать тесты и как применять React Testing Library в процессе разработки.
Комментарии (6)
markelov69
25.05.2022 17:20React hooks, как не выстрелить себе в ноги?
- Легко, использовать MobX.Carduelis
25.05.2022 17:33+1Но... mobx рекомендовано использовать с хуками =)
markelov69
25.05.2022 17:41-4Если своей головы на плечах нет, то используйте все по примерам из интернета, в целом это стандартная практика для 99% разработчиков которые в профессии чисто из-за зарплаты
mSnus
26.05.2022 03:22+6как работать с состоянием компонента, что такое "батчинг" (butching)
Butching - это когда сеньор-мясник разделывает джуна за то, что тот неправильно смерджил ветки. А то, что у вас - это batching.
Хук или по-русски крючок - это функция, которая вызывается в теле функционального компонента.
Хук (по-русски "навеска") - это функция, которая без написания классов позволяет навесить к компоненту какую-то дополнительную функциональность - хранение состояния и т.д.
Обратите внимание, необязательно вызывать функцию внутри useState, он сам это сделает: useState(getStoredState()) → useState(getStoredState)
В первом случае передаётся не функция, а результат её вызова.
ставьте палец вверх
легко!☝️
MVN63
Спасибо за попытку!
Было бы здОрово:
- пронумеровать примеры,
- пронумеровать строки в примерах,
- подчеркнуть, что хуки объявляются и вызываются именно в теле ФК (пояснить почему),
- в блоке про Асинхронность useState пояснить последний пример (почему работает только так).