
Статья поможет новичкам понять как работать с хуками, а также будет полезна и опытным разработчикам. Этой статьей открываю серию статей про хуки.
Использование хуков с одной стороны позволяет использовать методы жизненного цикла в функциональных компонентах и призваны улучшать производительность, что делает функциональные компоненты полноценным конкурентом классовых компонентов. С другой стороны, неправильное использование хуков приводит к лишним операциям и может свести на нет все преимущества функциональных компонентов.
В этой серии статей разберем основные хуки реакта и как их правильно использовать.
В серии статей поговорим про:
-
useState, как работать с состоянием компонента, что такое «батчинг» (butching) и для чего в качестве аргумента можно передать функцию;
-
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)}> установить true </button> <button onClick={() => setState(false)}> установить false </button> <button onClick={() => setState(prevState => !prevState)}> изменить состояние на противоположное </button> </div> ); };
state — это обычная переменная, с нем можно работать, как и с любой другой переменной: state.toString()Может быть любым типом данных. Не пытайтесь изменять состояние вручную state = newState, это нарушит работу вашего приложения. Единственный правильный путь — использовать функцию setState.
setState — это функция, которая принимает новое состояние (строки 8 и 11), либо функцию, которая принимает предыдущее состояние и возвращает новое состояние (строка 14). Главное — вызов функции запускает весь код функционального компонента повторно. В примере выше весь код, начиная с 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 раз, это называется «butching». Есть хорошая статья на эту тему.
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 в процессе разработки.
ссылка на оригинал статьи https://habr.com/ru/company/otus/blog/667706/
Добавить комментарий