Pull to refresh
824.77
OTUS
Цифровые навыки от ведущих экспертов

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

Reading time 8 min
Views 38K

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

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

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

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

  • 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).

Второй вариант - когда обрабатываем какие-то данные перед тем, как записать их в качестве начального состояния. Например, компонент принимает в качестве пропсов какие-то данные, но внутри себя, преобразовывает их в другую структуру.

import React, { useState, FC } from "react";

type TestComponentProps = {
	data: object;
}

const prepareData = (data: object): OtherData => {
  console.log('prepareData'); /* 1. как часто будет вызываться prepareData? */
  return otherData; // какие-то другие данные (не рабочий код)
}

export const TestComponent: FC<TestComponentProps> = ({ data }) => {
  const [state, setState] = useState(prepareData(data));
  
  return (
    <div>
      <div>{JSON.stringify(state)}</div>
      <button type="button" onClick={() => setState(['some key'])}>
	      set state
      </button>
    </div>
  );
};

Важно понимать, что при любом обновлении компонента, выполняется весь код этого компонента, в том числе и prepareData. Однако начальное значение состояние принимается только при монтировании компонента, при других обновлениях компонента - игнорируется. Потому функция prepareData будет выполняться всегда, хотя ее результат будет использован только 1 раз. Это ухудшает производительность. Что нужно сделать?

import React, { useState, FC } from "react";

type TestComponentProps = {
	data: object;
}

const prepareData = (data: object): OtherData => {
  console.log('prepareData');
  return otherData; // какие-то другие данные (не рабочий код)
}

export const TestComponent: FC<TestComponentProps> = ({ data }) => {
  // достаточно в качестве initialState передавать не prepareData(data)
  // а функцию, которая вернет prepareData(data)
  const [state, setState] = useState(() => prepareData(data));
  
  return (
    <div>
      <div>{JSON.stringify(state)}</div>
      <button type="button" onClick={() => setState(...)}>
	      set state
      </button>
    </div>
  );
};

Достаточно в качестве initialState передавать не prepareData(data), а функцию, которая вернет prepareData(data).

Заключение

Хук (по-русски крючок) - это функция. Как и любая функция, он может принимать аргументы и возвращать значение. Реакт регистрирует все хуки компонента, потому они должны быть до любых условий (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 в процессе разработки.

Зарегистрироваться на урок.

Tags:
Hubs:
+6
Comments 7
Comments Comments 7

Articles

Information

Website
otus.ru
Registered
Founded
Employees
101–200 employees
Location
Россия
Representative
OTUS