React hooks, как не выстрелить себе в ноги. Часть 1: работа с состоянием

от автора

Статья поможет новичкам понять как работать с хуками, а также будет полезна и опытным разработчикам. Этой статьей открываю серию статей про хуки.

Использование хуков с одной стороны позволяет использовать методы жизненного цикла в функциональных компонентах и призваны улучшать производительность, что делает функциональные компоненты полноценным конкурентом классовых компонентов. С другой стороны, неправильное использование хуков приводит к лишним операциям и может свести на нет все преимущества функциональных компонентов.

В этой серии статей разберем основные хуки реакта и как их правильно использовать.

В серии статей поговорим про:

  • 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *