Состояние: память компонента

Компоненты часто должны изменять то, что отображается на экране как результат взаимодействия. Ввод в форму должен обновлять поле ввода, нажатие кнопки “далее” на карусели изображений должно изменять отображаемое изображение, нажатие кнопки “купить” должно добавлять товар в корзину. Компоненты должны “запоминать”: текущее значение ввода, текущее изображение, корзину. В React такой компонент-специфической памятью называется состояние.

You will learn

  • Как добавить переменную состояния с помощью хука useState
  • Какую пару значений возвращает хук useState
  • Как добавить более одной переменной состояния
  • Почему состояние называется локальным

Когда обычной переменной недостаточно

Вот компонент, который рендерит изображение скульптуры. При нажатии на кнопку “Next” должна отображаться следующая скульптура, изменяя index на 1, затем на 2 и так далее. Однако это не сработает (вы можете попробовать!):

import { sculptureList } from './data.js';

export default function Gallery() {
  let index = 0;

  function handleClick() {
    index = index + 1;
  }

  let sculpture = sculptureList[index];
  return (
    <>
      <button onClick={handleClick}>
        Next
      </button>
      <h2>
        <i>{sculpture.name} </i> 
        by {sculpture.artist}
      </h2>
      <h3>  
        ({index + 1} of {sculptureList.length})
      </h3>
      <img 
        src={sculpture.url} 
        alt={sculpture.alt}
      />
      <p>
        {sculpture.description}
      </p>
    </>
  );
}

Обработчик событий handleClick обновляет локальную переменную index. Но две вещи мешают нам увидеть изменения:

  1. Локальные переменные не сохраняются между рендерами. Когда React рендерит этот компонент второй раз, он рендерит его заново - не учитывая никакие изменения локальных переменных.
  2. Изменения локальных переменных не запускают повторные рендеры. React не понимает, что нужно перерисовать компонент с новыми данными.

Чтобы обновить компонент с новыми данными, нужно сделать две вещи:

  1. Сохранить данные между рендерами.
  2. Запустить отрисовку React компонента с новыми данными (перерисовка).

Хук useState обеспечивает эти две вещи:

  1. Переменная состояния для сохранения данных между рендерами.
  2. Функция установки состояния для обновления переменной и запуска повторного рендеринга компонента.

Добавление переменной состояния

Для того чтобы добавить переменную состояния, импортируйте useState из React в начале файла:

import { useState } from 'react';

Далее, замените эту строку:

let index = 0;

на эту:

const [index, setIndex] = useState(0);

index - это переменная состояния, а setIndex - функция установки состояния.

Синтакс [ и ] называется деструктуризацией массива и позволяет читать значения из массива. Массив, возвращаемый useState, всегда имеет ровно два элемента.

Вот как они работают вместе в handleClick:

function handleClick() {
setIndex(index + 1);
}

Now clicking the “Next” button switches the current sculpture: Теперь нажатие кнопки “Next” переключает текущую скульптуру:

import { useState } from 'react';
import { sculptureList } from './data.js';

export default function Gallery() {
  const [index, setIndex] = useState(0);

  function handleClick() {
    setIndex(index + 1);
  }

  let sculpture = sculptureList[index];
  return (
    <>
      <button onClick={handleClick}>
        Next
      </button>
      <h2>
        <i>{sculpture.name} </i> 
        by {sculpture.artist}
      </h2>
      <h3>  
        ({index + 1} of {sculptureList.length})
      </h3>
      <img 
        src={sculpture.url} 
        alt={sculpture.alt}
      />
      <p>
        {sculpture.description}
      </p>
    </>
  );
}

Встречай твой первый хук

В React useState, а также любая другая функция, начинающаяся с ”use”, называется хуком.

Хуки — это специальные функции, доступные только во время рендеринга React (о чем мы подробнее расскажем на следующей странице). Они позволяют вам подключаться (hook into) к различным функциям React.

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

Pitfall

Хуки-функции начинаются с use и могут быть вызваны только на верхнем уровне ваших компонентов или в ваших собственных хуках. Вы не можете вызывать хуки внутри условий, циклов или других вложенных функций. Хуки — это функции, но полезно думать о них как о безусловных объявлениях о потребностях вашего компонента. Вы “используете” функции React на верхнем уровне вашего компонента, подобно тому, как вы “импортируете” модули в начале файла.

Анатомия useState

Когда ты вызываешь useState, ты говоришь React, что ты хочешь, чтобы этот компонент запомнил что-то:

const [index, setIndex] = useState(0);

В данном случае ты хочешь, чтобы React запомнил index.

Note

Соглашение состоит в том, чтобы называть эту пару как const [something, setSomething]. Вы можете назвать это как угодно, но соглашения упрощают понимание в разных проектах.

Единствеенный аргумент useState — это начальное значение вашей переменной состояния. В этом примере начальное значение index установлено в 0 с помощью useState(0).

Каждый раз при рендеринге вашего компонента useState дает вам массив, содержащий два значения:

  1. Переменная состояния (index) со значением, которое вы сохранили.
  2. Функция установки состояния (setIndex), которая может обновить переменную состояния и заставить React снова отрендерить компонент.

Вот как это происходит в действии:

const [index, setIndex] = useState(0);
  1. Твой компонент рендерится в первый раз. Поскольку вы передали 0 в useState в качестве начального значения для index, он вернет [0, setIndex]. React запоминает, что 0 — это последнее значение состояния.
  2. Ты обновляешь состояние. Когда пользователь нажимает кнопку, вызывается setIndex(index + 1). index равен 0, поэтому это setIndex(1). Это говорит React запомнить, что index теперь равен 1 и вызвать рендеринг.
  3. Второй рендеринг твоего компонента. React по-прежнему видит useState(0), но поскольку React запомнил, что вы установили index в 1, он возвращает [1, setIndex].
  4. И так далее!

Предоставление компоненту несколько переменных состояния

Ты можешь иметь столько переменных состояния разных типов, сколько захочешь в одном компоненте. Этот компонент имеет две переменные состояния: число index и логическое значение showMore, которое переключается, когда вы нажимаете “Show details”:

import { useState } from 'react';
import { sculptureList } from './data.js';

export default function Gallery() {
  const [index, setIndex] = useState(0);
  const [showMore, setShowMore] = useState(false);

  function handleNextClick() {
    setIndex(index + 1);
  }

  function handleMoreClick() {
    setShowMore(!showMore);
  }

  let sculpture = sculptureList[index];
  return (
    <>
      <button onClick={handleNextClick}>
        Next
      </button>
      <h2>
        <i>{sculpture.name} </i> 
        by {sculpture.artist}
      </h2>
      <h3>  
        ({index + 1} of {sculptureList.length})
      </h3>
      <button onClick={handleMoreClick}>
        {showMore ? 'Hide' : 'Show'} details
      </button>
      {showMore && <p>{sculpture.description}</p>}
      <img 
        src={sculpture.url} 
        alt={sculpture.alt}
      />
    </>
  );
}

Это хорошая идея иметь несколько переменных состояния, если их состояние не связано, например, index и showMore в этом примере. Но если вы часто изменяете две переменные состояния вместе, может быть проще объединить их в одну. Например, если у вас есть форма с множеством полей, удобнее иметь одну переменную состояния, которая содержит объект, чем переменную состояния для каждого поля. Прочитайте Выбор структуры состояния для получения дополнительных советов.

Deep Dive

Как React узнает, какое состояние вернуть?

Вероятно ты заметил, что вызов useState не получает никакой информации о том на какое состоянии он ссылается. Нет “идентификатора”, который передается в useState, так что как он знает, на какую из переменных состояния ссылаться? Он полагается на какую-то магию, как разбор ваших функций? Ответ - нет.

Вместо этого, чтобы обеспечить их лаконичный синтаксис, Хуки полагаются на стабильный порядок вызова на каждой отрисовке одного и того же компонента. Это хорошо работает на практике, потому что если вы следуете правилу выше (“вызывайте Хуки только на верхнем уровне”), Хуки всегда будут вызываться в одном и том же порядке. Кроме того, плагин линтера позволяет обнаружить большинство ошибок.

Под капотом React хранит массив пар состояний для каждого компонента. Он также поддерживает текущий индекс пары, который устанавливается на 0 перед отрисовкой. Каждый раз, когда вы вызываете useState, React дает вам следующую пару состояний и увеличивает индекс. Вы можете прочитать больше об этом механизме в React Hooks: Not Magic, Just Arrays.

Этот пример не использует React, но он дает вам представление о том, как useState работает внутри:

let componentHooks = [];
let currentHookIndex = 0;

// Как работает useState внутри React (упрощенно).
function useState(initialState) {
  let pair = componentHooks[currentHookIndex];
  if (pair) {
    // Это не первая отрисовка,
    // поэтому пара состояний уже существует.
    // Вернуть его и подготовиться к следующему вызову Хука.
    currentHookIndex++;
    return pair;
  }

  // Это первая отрисовка,
  // поэтому создаем пару состояний и сохраняем ее.
  pair = [initialState, setState];

  function setState(nextState) {
    // Когда пользователь запрашивает изменение состояния,
    // поместить новое значение в пару.
    pair[0] = nextState;
    updateDOM();
  }

  // Сохраним пару для будущих отрисовок
  // и подготовимся к следующему вызову Хука.
  componentHooks[currentHookIndex] = pair;
  currentHookIndex++;
  return pair;
}

function Gallery() {
  // Каждый вызов useState() получит следующую пару.
  const [index, setIndex] = useState(0);
  const [showMore, setShowMore] = useState(false);

  function handleNextClick() {
    setIndex(index + 1);
  }

  function handleMoreClick() {
    setShowMore(!showMore);
  }

  let sculpture = sculptureList[index];
  // Этот пример не использует React, поэтому
  // возвращаем объект вывода вместо JSX.
  return {
    onNextClick: handleNextClick,
    onMoreClick: handleMoreClick,
    header: `${sculpture.name} by ${sculpture.artist}`,
    counter: `${index + 1} of ${sculptureList.length}`,
    more: `${showMore ? 'Hide' : 'Show'} details`,
    description: showMore ? sculpture.description : null,
    imageSrc: sculpture.url,
    imageAlt: sculpture.alt
  };
}

function updateDOM() {
  // Сбразываем текущий индекс Хука
  // перед отрисовкой компонента.
  currentHookIndex = 0;
  let output = Gallery();

  // Обновляем DOM, чтобы он соответствовал выводу.
  // Это то, что React делает за вас.
  nextButton.onclick = output.onNextClick;
  header.textContent = output.header;
  moreButton.onclick = output.onMoreClick;
  moreButton.textContent = output.more;
  image.src = output.imageSrc;
  image.alt = output.imageAlt;
  if (output.description !== null) {
    description.textContent = output.description;
    description.style.display = '';
  } else {
    description.style.display = 'none';
  }
}

let nextButton = document.getElementById('nextButton');
let header = document.getElementById('header');
let moreButton = document.getElementById('moreButton');
let description = document.getElementById('description');
let image = document.getElementById('image');
let sculptureList = [{
  name: 'Homenaje a la Neurocirugía',
  artist: 'Marta Colvin Andrade',
  description: 'Although Colvin is predominantly known for abstract themes that allude to pre-Hispanic symbols, this gigantic sculpture, an homage to neurosurgery, is one of her most recognizable public art pieces.',
  url: 'https://i.imgur.com/Mx7dA2Y.jpg',
  alt: 'A bronze statue of two crossed hands delicately holding a human brain in their fingertips.'  
}, {
  name: 'Floralis Genérica',
  artist: 'Eduardo Catalano',
  description: 'This enormous (75 ft. or 23m) silver flower is located in Buenos Aires. It is designed to move, closing its petals in the evening or when strong winds blow and opening them in the morning.',
  url: 'https://i.imgur.com/ZF6s192m.jpg',
  alt: 'A gigantic metallic flower sculpture with reflective mirror-like petals and strong stamens.'
}, {
  name: 'Eternal Presence',
  artist: 'John Woodrow Wilson',
  description: 'Wilson was known for his preoccupation with equality, social justice, as well as the essential and spiritual qualities of humankind. This massive (7ft. or 2,13m) bronze represents what he described as "a symbolic Black presence infused with a sense of universal humanity."',
  url: 'https://i.imgur.com/aTtVpES.jpg',
  alt: 'The sculpture depicting a human head seems ever-present and solemn. It radiates calm and serenity.'
}, {
  name: 'Moai',
  artist: 'Unknown Artist',
  description: 'Located on the Easter Island, there are 1,000 moai, or extant monumental statues, created by the early Rapa Nui people, which some believe represented deified ancestors.',
  url: 'https://i.imgur.com/RCwLEoQm.jpg',
  alt: 'Three monumental stone busts with the heads that are disproportionately large with somber faces.'
}, {
  name: 'Blue Nana',
  artist: 'Niki de Saint Phalle',
  description: 'The Nanas are triumphant creatures, symbols of femininity and maternity. Initially, Saint Phalle used fabric and found objects for the Nanas, and later on introduced polyester to achieve a more vibrant effect.',
  url: 'https://i.imgur.com/Sd1AgUOm.jpg',
  alt: 'A large mosaic sculpture of a whimsical dancing female figure in a colorful costume emanating joy.'
}, {
  name: 'Ultimate Form',
  artist: 'Barbara Hepworth',
  description: 'This abstract bronze sculpture is a part of The Family of Man series located at Yorkshire Sculpture Park. Hepworth chose not to create literal representations of the world but developed abstract forms inspired by people and landscapes.',
  url: 'https://i.imgur.com/2heNQDcm.jpg',
  alt: 'A tall sculpture made of three elements stacked on each other reminding of a human figure.'
}, {
  name: 'Cavaliere',
  artist: 'Lamidi Olonade Fakeye',
  description: "Descended from four generations of woodcarvers, Fakeye's work blended traditional and contemporary Yoruba themes.",
  url: 'https://i.imgur.com/wIdGuZwm.png',
  alt: 'An intricate wood sculpture of a warrior with a focused face on a horse adorned with patterns.'
}, {
  name: 'Big Bellies',
  artist: 'Alina Szapocznikow',
  description: "Szapocznikow is known for her sculptures of the fragmented body as a metaphor for the fragility and impermanence of youth and beauty. This sculpture depicts two very realistic large bellies stacked on top of each other, each around five feet (1,5m) tall.",
  url: 'https://i.imgur.com/AlHTAdDm.jpg',
  alt: 'The sculpture reminds a cascade of folds, quite different from bellies in classical sculptures.'
}, {
  name: 'Terracotta Army',
  artist: 'Unknown Artist',
  description: 'The Terracotta Army is a collection of terracotta sculptures depicting the armies of Qin Shi Huang, the first Emperor of China. The army consisted of more than 8,000 soldiers, 130 chariots with 520 horses, and 150 cavalry horses.',
  url: 'https://i.imgur.com/HMFmH6m.jpg',
  alt: '12 terracotta sculptures of solemn warriors, each with a unique facial expression and armor.'
}, {
  name: 'Lunar Landscape',
  artist: 'Louise Nevelson',
  description: 'Nevelson was known for scavenging objects from New York City debris, which she would later assemble into monumental constructions. In this one, she used disparate parts like a bedpost, juggling pin, and seat fragment, nailing and gluing them into boxes that reflect the influence of Cubism’s geometric abstraction of space and form.',
  url: 'https://i.imgur.com/rN7hY6om.jpg',
  alt: 'A black matte sculpture where the individual elements are initially indistinguishable.'
}, {
  name: 'Aureole',
  artist: 'Ranjani Shettar',
  description: 'Shettar merges the traditional and the modern, the natural and the industrial. Her art focuses on the relationship between man and nature. Her work was described as compelling both abstractly and figuratively, gravity defying, and a "fine synthesis of unlikely materials."',
  url: 'https://i.imgur.com/okTpbHhm.jpg',
  alt: 'A pale wire-like sculpture mounted on concrete wall and descending on the floor. It appears light.'
}, {
  name: 'Hippos',
  artist: 'Taipei Zoo',
  description: 'The Taipei Zoo commissioned a Hippo Square featuring submerged hippos at play.',
  url: 'https://i.imgur.com/6o5Vuyu.jpg',
  alt: 'A group of bronze hippo sculptures emerging from the sett sidewalk as if they were swimming.'
}];

// Делаем интерфейс соответствующим начальному состоянию.
updateDOM();

Ты не обязан понимать это, чтобы использовать React, но тебе может быть полезна эта ментальная модель.

Состояние изолировано и приватно

Состояние локально для экземпляра компонента на экране. Другими словами, если вы отрисуете один и тот же компонент дважды, каждая копия будет иметь полностью изолированное состояние! Изменение одного из них не повлияет на другой.

В этом примере компонент Gallery из предыдущего примера отрисован дважды без изменений в логике. Попробуйте нажать на кнопки внутри каждой галереи. Обратите внимание, что их состояние независимо:

import Gallery from './Gallery.js';

export default function Page() {
  return (
    <div className="Page">
      <Gallery />
      <Gallery />
    </div>
  );
}

Это то, что делает состояние отличным от обычных переменных, которые вы можете объявить в верхней части своего модуля. Состояние не привязано к конкретному вызову функции или месту в коде, но оно «локально» для конкретного места на экране. Вы отрисовали два компонента <Gallery />, поэтому их состояние хранится отдельно.

Также обратите внимание, что компонент Page ничего не знает о состоянии Gallery или даже о том, есть ли оно. В отличие от свойств, состояние полностью приватно для компонента, объявляющего его. Родительский компонент не может его изменить. Это позволяет вам добавлять состояние в любой компонент или удалять его без влияния на остальные компоненты.

Что если вы хотите, чтобы обе галереи синхронизировали свои состояния? Правильный способ сделать это в React - удалить состояние из дочерних компонентов и добавить его в их ближайший общий родитель. Следующие несколько страниц будут посвящены организации состояния одного компонента, но мы вернемся к этой теме в Sharing State Between Components.

Recap

  • Используйте переменную состояния, когда компоненту нужно «запомнить» некоторую информацию между рендерами.
  • Переменные состония декларируются вызовом хука useState.
  • Hooks are special functions that start with use. They let you “hook into” React features like state.
  • Хуки это специальные функции, которые начинаются с use. Они позволяют вам подключаться к функциям React, таким как состояние.
  • Хуки могут напоминать вам импорты: они должны быть вызваны безусловно. Вызов хуков, включая useState, допустим только на верхнем уровне компонента или другого хука.
  • Вызов хука useState возвращает пару значений: текущее состояние и функцию для его обновления.
  • Вы можете иметь более одной переменной состояния. Внутри React сопоставляет их по порядку.
  • Состояние приватно для компонента. Если вы отрисуете его в двух местах, каждая копия получит свое собственное состояние.

Когда вы нажимаете «Next» на последней скульптуре, код ломается. Исправьте логику, чтобы предотвратить сбой. Вы можете сделать это, добавив дополнительную логику в обработчик событий или отключив кнопку, когда действие невозможно.

После исправления сбоя добавьте кнопку «Предыдущая», которая показывает предыдущую скульптуру. Он не должен ломаться на первой скульптуре.

import { useState } from 'react';
import { sculptureList } from './data.js';

export default function Gallery() {
  const [index, setIndex] = useState(0);
  const [showMore, setShowMore] = useState(false);

  function handleNextClick() {
    setIndex(index + 1);
  }

  function handleMoreClick() {
    setShowMore(!showMore);
  }

  let sculpture = sculptureList[index];
  return (
    <>
      <button onClick={handleNextClick}>
        Next
      </button>
      <h2>
        <i>{sculpture.name} </i> 
        by {sculpture.artist}
      </h2>
      <h3>  
        ({index + 1} of {sculptureList.length})
      </h3>
      <button onClick={handleMoreClick}>
        {showMore ? 'Hide' : 'Show'} details
      </button>
      {showMore && <p>{sculpture.description}</p>}
      <img 
        src={sculpture.url} 
        alt={sculpture.alt}
      />
    </>
  );
}