useState vs useRef

Published on
Authors

В этой статье объясняются хуки React useState и useRef. Вы узнаете об их базовом использовании и познакомитесь с различными вариантами использования обоих хуков.

Понимание хука useState

Хук useState позволяет создавать состояние для функциональных компонентов. До React 16.8 локальное состояние компонента было возможно только с компонентами на основе классов.

Взгляните на следующий код

import { useState } from "react";
function AppDemo1() {
  const stateWithUpdater = useState(true);
  const darkMode = stateWithUpdater[0];
  const darkModeUpdater = stateWithUpdater[1];
  return (
    <div>
      <p>{darkMode ? "dark mode on" : "dark mode off"}</p>
      <button onClick={() => darkModeUpdater(!darkMode)}>
        toggle dark mode
      </button>
    </div>
  );
}

Хук useState возвращает массив с двумя элементами. В этом примере мы реализуем логическое состояние компонента и инициализируем наш Hook значением true.

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

Первый элемент массива представляет фактическое состояние, а второй элемент представляет собой функцию обновления состояния. Обработчик onClick демонстрирует, как использовать функцию обновления (darkModeUpdate) для изменения переменной состояния (darkMode). Важно точно так же обновлять свое состояние. Следующий код недопустим: 

darkMode = true;

Если у вас есть опыт работы с useState Hook, вы можете задаться вопросом о синтаксисе моего примера. По умолчанию используются возвращенные элементы массива с помощью array destructuring.

const [darkMode, setDarkMode] = useState(true);

Напоминаем, что очень важно следовать правилам хуков при использовании любого хука, а не только useState или useRef: 

  • -Хуки следует вызывать только с верхнего уровня вашей функции React.
  •  Хуки не должны вызываться из вложенного кода (например, циклов, условий)
  •  Хуки также могут вызываться на верхнем уровне из пользовательских хуков.

Теперь, когда мы рассмотрели основы, давайте рассмотрим все аспекты хука с помощью следующего примера кода.

import { useState } from "react";
import "./styles.css";
function AppDemo2() {
  console.log("render App");
  const [darkMode, setDarkMode] = useState(false);
  return (
    <div className={`App ${darkMode && "dark-mode"}`}>
      <h1>The useState hook</h1>
      <h2>Click the button to toggle the state</h2>
      <button
        onClick={() => {
          setDarkMode(!darkMode);
        }}
      >
        toggle dark mode
      </button>
    </div>
  );
}

Если для darkMode установлено значение true, то к className добавляется дополнительный класс CSS (dark-mode), а цвета фона и текста инвертируются. Как видно из вывода консоли в записи, каждый раз, когда состояние изменяется, соответствующий компонент повторно визуализируется.

React DevTools особенно полезен здесь для визуального выделения обновлений при рендеринге компонентов. В последней записи вы можете увидеть мигающую рамку вокруг компонента, которая уведомляет вас о другом цикле рендеринга компонента.

В следующем примере заголовки выделены в отдельный компонент React (Description).

import { useState } from "react";
import "./styles.css";
function AppDemo3() {
  console.log("render App");
  const [darkMode, setDarkMode] = useState(false);
  return (
    <div className={`App ${darkMode && "dark-mode"}`}>
      <Description />
      <button
        onClick={() => {
          setDarkMode(!darkMode);
        }}
      >
        toggle dark mode
      </button>
    </div>
  );
}
const Description = () => {
  console.log("render Description");
  return (
    <>
      <h1>The useState hook</h1>
      <h2>Click the button to toggle the state</h2>
    </>
  );
};

Компонент App отображается всякий раз, когда пользователь нажимает кнопку, потому что соответствующий обработчик кликов обновляет переменную состояния darkMode. Кроме того, рендерится и дочерний компонент Description.

На следующей диаграмме показано, что изменение состояния вызывает цикл рендеринга.

Почему важно понимать жизненный цикл React Hooks? С одной стороны, состояние сохраняется во время рендеринга до тех пор, пока вы не обновляете состояние с помощью функции обновления, которая сама по себе запускает обновленный цикл рендеринга.

Использование хука useState с useEffect

Еще одна важная концепция, которую необходимо понять, - это хук useEffect, который вам, скорее всего, придется использовать в своем приложении для вызова асинхронного кода (например, для выборки данных). Как вы можете видеть на предыдущей диаграмме, хуки useState и useEffect тесно связаны, поскольку изменения состояния могут вызывать эффекты.

Давайте посмотрим на следующий пример. Мы вводим две дополнительные переменные состояния: load и lang. Эффект вызывается всякий раз, когда изменяется свойство url. Он извлекает языковую строку (en или de) и обновляет состояние с помощью функции обновления setLang.

В зависимости от языка отображается строка на английском или немецком языке внутри заголовка. Кроме того, в процессе выборки устанавливается состояние загрузки, и в зависимости от значения (истина или ложь) вместо заголовка отображается индикатор загрузки.

import { useEffect, useState } from "react";
import "./styles.css";
  function App4({ url }) {
  console.log("render App");
  const [loading, setLoading] = useState(true);
  const [lang, setLang] = useState("de");
  const [darkMode, setDarkMode] = useState(false);
  useEffect(() => {
    console.log("useEffect");
    const fetchData = async function () {
      try {
        setLoading(true);
        const response = await axios.get(url);
        if (response.status === 200) {
          const { language } = response.data;
          setLang(language);
        }
      } catch (error) {
        throw error;
      } finally {
        setLoading(false);
      }
    };
    fetchData();
  }, [url]);
  return (
    <div className={`App ${darkMode && "dark-mode"}`}>
      {loading ? (
        <div>Loading...</div>
      ) : (
        <>
          <h1>
            {lang === "en"
              ? "The useState hook is awesome"
              : "Der useState Hook ist toll"}
          </h1>
          <button
            onClick={() => {
              setDarkMode(!darkMode);
            }}
          >
            toggle dark mode
          </button>
        </>
      )}
    </div>
  );
}

Представим, что мы хотим переключать темный режим всякий раз, когда выбираем текущий язык. Мы добавляем вызов средства обновления setDarkMode после обновления языка. Кроме того, нам нужно добавить состояние darkMode как зависимость к массиву зависимостей эффекта.

import { useEffect, useState } from "react";
import "./styles.css";
function AppDemo5({ url }) {
  console.log("render App");
  const [loading, setLoading] = useState(true);
  const [lang, setLang] = useState("de");
  const [darkMode, setDarkMode] = useState(false);
  useEffect(() => {
    console.log("useEffect");
    const fetchData = async function () {
      try {
        setLoading(true);
        const response = await axios.get(url);
        if (response.status === 200) {
          const { language } = response.data;
          setLang(language);
          setDarkMode(!darkMode);
        }
      } catch (error) {
        throw error;
      } finally {
        setLoading(false);
      }
    };
    fetchData();
  }, [url, darkMode]);
  return (
    <div className={`App ${darkMode && "dark-mode"}`}>
      {loading ? (
        <div>Loading...</div>
      ) : (
        <>
          <h1>
            {lang === "en"
              ? "The useState hook is awesome"
              : "Der useState Hook ist toll"}
          </h1>
          <button
            onClick={() => {
              setDarkMode(!darkMode);
            }}
          >
            toggle dark mode
          </button>
        </>
      )}
    </div>
  );
}

К сожалению, мы вызвали бесконечный цикл.

Это почему? Поскольку мы добавили darkMode в массив зависимостей эффекта и обновили это точное состояние внутри эффекта, эффект снова вызывается, снова обновляет состояние, и это продолжается и продолжается.

Но выход есть! Мы можем избежать darkMode как зависимости эффекта, вычислив новое состояние из предыдущего состояния. Мы вызываем программу обновления setDarkMode по-другому, передавая функцию, которая имеет предыдущее состояние в качестве аргумента.

Обновленная реализация useEffect выглядит так:

  useEffect(() => {
    console.log("useEffect");
    const fetchData = async function () {
      try {
        setLoading(true);
        const response = await axios.get(url);
        if (response.status === 200) {
          const { language } = response.data;
          setLang(language);
          setDarkMode((previous) => !previous); // no access of darkMode state
        }
      } catch (error) {
        throw error;
      } finally {
        setLoading(false);
      }
    };
    fetchData();
  }, [url]); // no darkMode dependency

Отличия от компонентов на основе классов

Если вы давно используете React или в настоящее время работаете над устаревшим кодом, вы знаете компоненты на основе классов. С компонентами на основе классов у вас есть один единственный объект, представляющий состояние компонента.  Чтобы обновить один фрагмент общего состояния, вы можете использовать общий [setState] 

(https://reactjs.org/docs/state-and-lifecycle.html) метод.

Представьте, что мы хотим обновить только переменную состояния darkMode. Затем вы можете просто поместить обновленное свойство в объект; остальное состояние остается неизменным.

this.setState({darkMode: false});

Однако с функциональными компонентами предпочтительным способом является использование атомарных переменных состояния, которые можно обновлять индивидуально. В противном случае вы можете быстро оказаться в долине слез.

По сравнению с AppDemo6 следующий компонент (AppDemo7) был переработан только в отношении управления состоянием. Вместо трех атомарных переменных состояния с примитивными типами данных мы используем один объект состояния (state).

import { useEffect, useState } from "react";
import "./styles.css";
function AppDemo7({ url }) {
  const initialState = {
    loading: true,
    lang: "de",
    darkMode: true
  };
  const [state, setState] = useState(initialState);
  console.log("render App", state);
  useEffect(() => {
    console.log("useEffect");
    const fetchData = async function () {
      try {
        setState((prev) => ({
          loading: true,
          lang: prev.lang,
          darkMode: prev.darkMode
        }));
        const response = await axios.get(url);
        if (response.status === 200) {
          const { language } = response.data;
          setState((prev) => ({
            lang: language,
            darkMode: !prev.darkMode,
            loading: prev.loading
          }));
        }
      } catch (error) {
        throw error;
      } finally {
        setState((prev) => ({
          loading: false,
          lang: prev.lang,
          darkMode: prev.darkMode
        }));
      }
    };
    fetchData();
  }, [url]);
  return (
    <div className={`App ${state.darkMode && "dark-mode"}`}>
      {state.loading ? (
        <div>Loading...</div>
      ) : (
        <>
          <h1>
            {state.lang === "en"
              ? "The useState hook is awesome"
              : "Der useState Hook ist toll"}
          </h1>
          <button
            onClick={() => {
              setState((prev) => ({
                darkMode: !prev.darkMode,
                // lang: prev.lang,
                loading: prev.loading
              }));
            }}
          >
            toggle dark mode
          </button>
        </>
      )}
    </div>
  );
}

Как видите, код запутан, и его сложно поддерживать. Он также включает ошибку, проиллюстрированную закомментированным свойством в обработчике onClick. Когда пользователь нажимает кнопку, общее состояние вычисляется неправильно.

В этом случае свойство lang отсутствует. Это приводит к ошибке, из-за которой текст отображается на немецком языке, поскольку state.lang не определен.