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

Компоненти часто потребують змінювати те, що на екрані, унаслідок взаємодії. Введення у формі має оновлювати поле введення, натискання на кнопку “Далі” у каруселі зображень — змінювати відображуване зображення, а натискання на кнопку “Купити” — додавати продукт до кошика. Компонентам потрібно “пам’ятати” все це: поточне значення у полі введення, поточне зображення, продукти у кошику. У React цей вид пам’яті певного компонента називається стан.

You will learn

  • Як додати змінну стану за допомогою хука useState
  • Яку пару значень повертає хук useState
  • Як додати більш ніж одну змінну стану
  • Чому стан називають локальним

Коли звичайної змінної недостатньо

Натискання на кнопку “Наступна” повинно показати наступну скульптуру, змінивши 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}>
        Наступна
      </button>
      <h2>
        <i>{sculpture.name} </i>{sculpture.artist}
      </h2>
      <h3>  
        ({index + 1} із {sculptureList.length})
      </h3>
      <img 
        src={sculpture.url} 
        alt={sculpture.alt}
      />
      <p>
        {sculpture.description}
      </p>
    </>
  );
}

Обробник подій handleClick оновлює локальну змінну — index. Але дві обставини заважають бачити цю зміну:

  1. Локальні змінні не зберігаються (don’t persist) між рендерами. Коли React рендерить цей компонент вдруге, він рендерить його з нуля — без врахування жодних змін у локальних змінних.
  2. Зміни у локальних змінних не викликають рендер. React не усвідомлює, що йому потрібно знову рендерити компонент із новими даними.

Щоб оновити компонент новими даними, потрібно виконати дві умови:

  1. Зберегти ці дані між рендерами.
  2. Спонукати React рендерити компонент з новими даними (повторний рендеринг).

Хук useState надає ці два елементи:

  1. Змінна стану для збереження даних між рендерами.
  2. Функція встановлення стану, щоб оновити змінну та спонукати React рендерити компонент повторно.

Додавання змінної стану

Щоб додати змінну стану, імпортуйте useState із React на початку файлу:

import { useState } from 'react';

Потім замініть цей рядок:

let index = 0;

на

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

де index — це змінна стану, а setIndex — функція встановлення.

Тут синтаксис [ та ] називається деструктуризацією масиву і дає вам змогу отримати значення з масиву. Повернений із useState масив завжди має лише два елементи.

Ось як вони працюють разом у функції handleClick:

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

Тепер натискання кнопки “Наступна” змінює поточну скульптуру:

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}>
        Наступна
      </button>
      <h2>
        <i>{sculpture.name} </i>{sculpture.artist}
      </h2>
      <h3>  
        ({index + 1} із {sculptureList.length})
      </h3>
      <img 
        src={sculpture.url} 
        alt={sculpture.alt}
      />
      <p>
        {sculpture.description}
      </p>
    </>
  );
}

Зустрічайте свій перший хук

У React useState, як і будь-яка інша функція, що починається з “use”, називається хуком.

Хуки — це спеціальні функції, які доступні лише під час рендерингу React (про що ми детальніше поговоримо на наступній сторінці). Вони дають вам змогу “чіплятися” до різних особливостей (features) React.

Стан — лише одна з цих особливостей, але ви познайомитеся з іншими хуками пізніше.

Pitfall

Хуки — функції, що починаються з use — можна викликати лише на верхньому рівні ваших компонентів або власних хуків. Ви не можете викликати хуки всередині умовних блоків, циклів або інших вкладених функцій. Хуки — це функції, але корисно думати про них як про безумовні декларації потреб вашого компонента. Ви “використовуєте” особливості (features) 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, яка перемикається, коли ви натискаєте “Показати подробиці”:

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}>
        Наступна
      </button>
      <h2>
        <i>{sculpture.name} </i>{sculpture.artist}
      </h2>
      <h3>  
        ({index + 1} із {sculptureList.length})
      </h3>
      <button onClick={handleMoreClick}>
        {showMore ? 'Приховати' : 'Показати'} подробиці
      </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: не магія, просто масиви”.

Цей приклад не використовує 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}${sculpture.artist}`,
    counter: `${index + 1} із ${sculptureList.length}`,
    more: `${showMore ? 'Приховати' : 'Показати'} подробиці`,
    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: 'Хоча Колвін переважно відома абстрактною тематикою з натяком на символи доіспанського періоду, ця величезна скульптура, присвячена нейрохірургії, є однією з її найвідоміших публічних робіт.',
  url: 'https://i.imgur.com/Mx7dA2Y.jpg',
  alt: 'Бронзова статуя двох перехрещених рук, які делікатно тримають людський мозок кінцями пальців.'  
}, {
  name: 'Рід квіткові (Floralis Genérica)',
  artist: 'Едуардо Каталано (Eduardo Catalano)',
  description: 'Ця велетенська (висотою 75 футів або 23 м) срібна квітка знаходиться в Буенос-Айресі. Вона рухома і може закривати свої пелюстки ввечері або під час сильного вітру та відкривати їх зранку.',
  url: 'https://i.imgur.com/ZF6s192m.jpg',
  alt: 'Велетенська металева скульптура квітки зі світловідбивними, схожими на дзеркало пелюстками і міцними тичинками.'
}, {
  name: 'Вічна присутність (Eternal Presence)',
  artist: 'Джон Вілсон (John Woodrow Wilson)',
  description: 'Вілсон був відомий своєю зацікавленістю у рівності, соціальній справедливості, а також в основних і духовних якостях людства. Ця масивна (висотою 7 футів або 2.13 м) бронзова скульптура зображає те, що він описав як "символічна присутність темношкірих, що наповнена почуттям універсальної людяності".',
  url: 'https://i.imgur.com/aTtVpES.jpg',
  alt: 'Скульптура людської голови, що здається всюдисущою і поважною. Вона випромінює спокій і мир.'
}, {
  name: 'Моаї (Moai)',
  artist: 'Невідомий автор',
  description: 'На острові Пасхи розташовано близько тисячі моаї — збережені до нашого часу монументальні статуї, створені першими рапануйцями, які, як деякі вважають, представляли "божественних" предків.',
  url: 'https://i.imgur.com/RCwLEoQm.jpg',
  alt: 'Три монументальні кам\'яні бюсти з головами, що є непропорційно великими і мають насуплені обличчя.'
}, {
  name: 'Синя "нана́" (Blue Nana)',
  artist: 'Нікі де Сен Фаль (Niki de Saint Phalle)',
  description: 'Нани (від фр. Nana — сленг: "жіночка") — це врочисті створіння, символи жіночності та материнства. Спочатку Сен Фаль використовувала тканину і наявні предмети (found objects) для нан, а потім додала поліестер, щоб зробити їх більш яскравими.',
  url: 'https://i.imgur.com/Sd1AgUOm.jpg',
  alt: 'Велика мозаїчна скульптура вигадливої жіночої постаті у кольоровому костюмі, що танцює і випромінює радість.'
}, {
  name: 'Довершена форма (Ultimate Form)',
  artist: 'Барбара Хепворт (Barbara Hepworth)',
  description: 'Ця абстрактна бронзова скульптура є частиною серії "Родина Людей" ("The Family of Man"), розташованої в парку скульптур у Йоркширі. Хепворт вирішила не створювати буквальні зображення світу, а розвивати абстрактні форми, натхненні людьми та пейзажами.',
  url: 'https://i.imgur.com/2heNQDcm.jpg',
  alt: 'Висока скульптура з трьох поставлених один на одного елементів, що нагадує постать людини.'
}, {
  name: 'Воїн (Cavaliere)',
  artist: 'Ламіді Факеє (Lamidi Olonade Fakeye)',
  description: "Роботи Факеє, різьбяра по дереву у четвертому поколінні, поєднують традиційні та сучасні теми народу Йоруба.",
  url: 'https://i.imgur.com/wIdGuZwm.png',
  alt: 'Деталізована дерев\'яна скульптура воїна із зосередженим обличчям на коні, прикрашеному візерунками.'
}, {
  name: 'Великі животи (Big Bellies)',
  artist: 'Аліна Шапочніков (Alina Szapocznikow)',
  description: 'Шапочніков відома своїми скульптурами фрагментів тіла як метафори крихкості та непостійності молодості і краси. Ця скульптура зображує два розташовані один над одним дуже реалістичних великих животи висотою приблизно п\'ять футів (1.5 м) кожен.',
  url: 'https://i.imgur.com/AlHTAdDm.jpg',
  alt: 'Скульптура нагадує каскад складок, що зовсім не схоже на животи у класичних скульптурах.'
}, {
  name: 'Теракотова армія (Terracotta Army)',
  artist: 'Невідомий автор',
  description: 'Теракотова армія — це колекція теракотових скульптур, що зображають війська Цінь Ши Хуан-ді, першого імператора Китаю. Армія складалася з понад 8 000 солдатів, 130 колісниць із 520 кіньми та 150 одиниць кінноти.',
  url: 'https://i.imgur.com/HMFmH6m.jpg',
  alt: '12 теракотових скульптур суворих воїнів, кожен з унікальним виразом обличчя та бронею.'
}, {
  name: 'Місячний пейзаж (Lunar Landscape)',
  artist: 'Луїза Невельсон (Louise Nevelson)',
  description: 'Невельсон була відома тим, що знаходила матеріали серед відходів Нью-Йорка, які вона потім збирала в монументальні споруди. У цій роботі вона використала різнорідні частини, як-от стійку ліжка, булаву для жонглювання та фрагмент сидіння, прибивши та вклеївши їх у коробки, які відображають вплив геометричної абстракції простору та форми кубізму.',
  url: 'https://i.imgur.com/rN7hY6om.jpg',
  alt: 'Чорна матова скульптура, в якій окремі елементи неможливо розрізнити на початку споглядання.'
}, {
  name: 'Ореол (Aureole)',
  artist: 'Ранджані Шеттар (Ranjani Shettar)',
  description: 'Шеттар поєднує традиційне та сучасне, природне та індустріальне. Її творчість зосереджена на стосунках між людиною та природою. Її роботи описують як переконливі і абстрактно, і образно, як ті, що кидають виклик гравітації, та як "тонкий синтез нетипових матеріалів".',
  url: 'https://i.imgur.com/okTpbHhm.jpg',
  alt: 'Бліда подібна до дротів скульптура, що встановлена на бетонній стіні та спадає додолу. Здається легкою.'
}, {
  name: 'Бегемоти (Hippos)',
  artist: 'Зоопарк Тайбею (Taipei Zoo)',
  description: 'Зоопарк Тайбею замовив площу бегемотів із зануреними бегемотами під час гри.',
  url: 'https://i.imgur.com/6o5Vuyu.jpg',
  alt: 'Група бронзових скульптур бегемота, що виринає з тротуару, ніби вони пливуть.'
}];

// Оновлюємо UI відповідно до початкового стану.
updateDOM();

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

Стан є ізольованим та приватним

Стан належить тільки до екземпляра компонента на екрані. Іншими словами, якщо ви рендерите один і той же компонент двічі, кожна копія матиме повністю ізольований стан! Зміна одного з них не вплине на інший.

У цьому прикладі компонент Gallery із попередньої частини рендериться двічі без змін у логіці. Спробуйте натиснути на кнопки всередині кожної з галерей. Зауважте, що їх стани незалежні:

import Gallery from './Gallery.js';

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

Ось що відрізняє стан від звичайних змінних, які ви могли б оголосити на початку вашого модуля. Стан не прив’язаний до конкретного виклику функції або місця у коді, але він “локальний” відносно конкретного місця на екрані. Ви рендерили два компоненти <Gallery />, тому їх стан зберігається окремо.

Також зауважте, що компонент Page “не знає” нічого ні про стан Gallery, ні навіть про його наявність. На відміну від пропсів стан доступний (private) лише компоненту, у якому оголошений. Батьківський компонент не може його змінити. Це дає вам змогу додавати стан до будь-якого компоненту або видаляти його, не впливаючи на решту компонентів.

А що як ви хочете, щоб обидві галереї синхронізували свої стани? Правильний спосіб зробити таке в React — прибрати стан з дочірніх компонентів і додати його до їх найближчого спільного батьківського компонента. Наступні кілька сторінок зосереджуються на організації стану окремого компонента, але ми повернемося до цієї теми в розділі “Спільний стан між компонентами”.

Recap

  • Використовуйте змінну стану, коли компоненту потрібно “пам’ятати” деяку інформацію між рендерами.
  • Змінні стану оголошуються через виклик хука useState.
  • Хуки — це спеціальні функції, які починаються з use. Вони дають вам змогу “чіплятися” до особливостей (features) React, наприклад, до стану.
  • Хуки можуть нагадувати імпорт: їх потрібно викликати безумовно. Виклик хуків, включно з useState, допускається тільки на верхньому рівні компонента або іншого хука.
  • Хук useState повертає пару значень: поточний стан і функцію для його оновлення.
  • У вас може бути більше однієї змінної стану. Всередині React зіставляє їх відповідно до порядку їх розміщення.
  • Стан є приватним для компонента. Якщо ви рендерите компонент у двох місцях, кожна копія буде мати свій власний стан.

Коли ви натискаєте “Наступна” на останній скульптурі, програма зазнає краху (crashes). Щоб запобігти йому, виправте логіку в коді. Це можна зробити, додавши певну логіку до обробника подій або деактивувавши (disabling) кнопку, коли дія не можлива.

Після виправлення проблеми додайте кнопку “Попередня”, яка показує попередню скульптуру. На першій скульптурі також не має бути краху.

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}>
        Наступна
      </button>
      <h2>
        <i>{sculpture.name} </i>{sculpture.artist}
      </h2>
      <h3>  
        ({index + 1} із {sculptureList.length})
      </h3>
      <button onClick={handleMoreClick}>
        {showMore ? 'Приховати' : 'Показати'} подробиці
      </button>
      {showMore && <p>{sculpture.description}</p>}
      <img 
        src={sculpture.url} 
        alt={sculpture.alt}
      />
    </>
  );
}