useState — хук React, який дозволяє додати змінну стану до вашого компонента.

const [state, setState] = useState(initialState)

Опис

useState(initialState)

Викликачте useState на верхньому рівні вашого компонента, щоб оголосити змінну стану.

import { useState } from 'react';

function MyComponent() {
const [age, setAge] = useState(28);
const [name, setName] = useState('Тейлор');
const [todos, setTodos] = useState(() => createTodos());
// ...

Зазвичай змінні стану називають як от [щось, setЩось], використовуючи деструктуризацію масиву.

Дивіться більше прикладів нижче.

Параметри

  • initialState: Значення, яке ви хочете встановити як початкове для стану. Це може бути значення будь-якого типу, але якщо це функція — діють особливі правила. Цей аргумент ігнорується після першого рендеру.
    • Якщо ви передаєте функцію як initialState, вона спрацює як як функція-ініціалізатор. Вона має бути чистою, без аргументів, і повертати будь-яке значення. React викличе її під час ініціалізації компонента й збереже те, що вона поверне, як початковий стан. Дивіться приклад нижче.

Результат

useState повертає масив, що містить рівно два значення:

  1. Поточний стан. Під час першого рендеру він дорівнює переданому initialState.
  2. Функція set, яка дозволяє оновити стан на нове значення і викликає повторний рендер.

Застереження

  • useState — це хук, тому його можна викликати лише на верхньому рівні вашого компонента або вашого власного хука. Не можна викликати його в циклах чи умовах. За потреби — перенесіть стан в новий компонент.
  • У режимі Strict Mode React буде викликати вашу функцію-ініціалізатор двічі, щоб допомогти виявити випадкові побічні ефекти. Це відбувається лише під час розробки і не впливає на продакшн. Якщо ваша функція-ініціалізатор чиста (якою вона і має бути), це не позначиться на поведінці компонента. Результат одного з викликів буде проігноровано.

Функції set, наприклад setSomething(nextState)

Функція set, яку повертає useState, дозволяє оновити стан на інше значення і викликати повторний рендер. Ви можете передати нове значення напряму або функцію, яка обчислює його з попереднього стану:

const [name, setName] = useState('Едвард');

function handleClick() {
setName('Тейлор');
setAge(a => a + 1);
// ...

Параметри

  • nextState: Значення, яке ви хочете встановити як новий стан. Це може бути значення будь-якого типу, але якщо це функція — діють особливі правила.
    • Якщо ви передасте функцію, React сприйме її як функцію-оновлювач. Вона має бути чистою, приймати поточний (ще не оновлений) стан як єдиний аргумент, і повертати нове значення стану. React додасть вашу функцію до черги оновлень і перерендерить компонент. Під час наступного рендеру React розрахує новий стан, послідовно застосовуючи всі функції з черги до попереднього стану. Дивіться приклад нижче.

Повернення

Функції set не повертають значення

Застереження

  • Функція set оновлює змінну стану лише для наступного рендеру. Якщо звернутися до змінної стану одразу після виклику set, ви все ще отримаєте попереднє значення, яке було актуальним до оновлення.

  • Якщо нове значення, яке ви передаєте, ідентичне поточному state (згідно з Object.is), React пропустить повторний рендер компонента та його нащадків. Це оптимізація. Хоча в деяких випадках React усе ж може викликати ваш компонент перед тим, як пропустити рендер нащадків, це не має вплинути на роботу вашого коду.

  • React обробляє оновлення стану пакетно. Він оновлює інтерфейс після завершення всіх обробників подій, які викликали свої функції set. Це запобігає кільком перерендерам під час однієї події. У рідкісних випадках, коли вам потрібно примусово оновити інтерфейс раніше (наприклад, щоб звернутися до DOM), ви можете скористатися flushSync.

  • Функція set має стабільну ідентичність, тож її часто не включають до залежностей у Effect. Але навіть якщо додати — це не спричинить повторного виклику Effect. Якщо лінтер дозволяє не вказувати залежність без помилок — так можна зробити. Дізнайтеся більше про видалення залежностей у Effect.

  • Виклик функції set під час рендеру дозволено лише всередині компонента, що наразі рендериться. React відкине його поточний результат і негайно спробує перерендерити компонент з новим станом. Такий підхід потрібен рідко, але його можна використати, щоб зберігати інформацію з попередніх рендерів. Дивіться приклад нижче.

  • У режимі Strict Mode React двічі викликає вашу функцію-оновлювач, щоб допомогти виявити випадкові побічні ефекти. Це відбувається лише під час розробки і не впливає на продакшн. Якщо ваша функція-оновлювач чиста (як і має бути), це не вплине на поведінку компонента. Результат одного з викликів буде проігноровано.


Використання

Додавання стану до компонента

Викликайте useState на верхньому рівні вашого компонента, щоб оголосити одну або кілька змінних стану.

import { useState } from 'react';

function MyComponent() {
const [age, setAge] = useState(42);
const [name, setName] = useState('Тейлор');
// ...

Зазвичай змінні стану називають у форматі [щось, setЩось], використовуючи деструктуризацію масиву.

useState повертає масив, що містить рівно два елементи:

  1. Поточний стан цієї змінної стану, який спочатку дорівнює початковому значенню, переданому вами.
  2. Функцію set, яка дає змогу змінювати цей стан у відповідь на взаємодію.

Щоб оновити інтерфейс, викличте функцію set з новим станом:

function handleClick() {
setName('Робін');
}

React збереже новий стан, повторно відрендерить компонент із цими значеннями та оновить інтерфейс.

Будьте обачні

Виклик функції set не змінює поточний стан у коді, що вже виконується:

function handleClick() {
setName('Робін');
console.log(name); // Все ще "Тейлор"!
}

Він впливає лише на те, що useState повертатиме починаючи з наступного рендеру.

Приклади використання useState

Приклад 1 із 4:
Лічильник (число)

У цьому прикладі змінна стану count зберігає число. Натискання кнопки збільшує його на одиницю.

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setCount(count + 1);
  }

  return (
    <button onClick={handleClick}>
      Ви натиснули на мене {count} раз(ів)
    </button>
  );
}


Оновлення стану на основі попереднього

Припустимо, значення age дорівнює 42. Цей обробник викликає setAge(age + 1) тричі:

function handleClick() {
setAge(age + 1); // setAge(42 + 1)
setAge(age + 1); // setAge(42 + 1)
setAge(age + 1); // setAge(42 + 1)
}

Однак після одного кліка age буде 43, а не 45! Це тому, що виклик функції set не оновлює змінну age у коді, що вже виконується. Тож кожен виклик setAge(age + 1) стає setAge(43).

Щоб вирішити цю проблему, можна передати до setAge функцію-оновлювач замість нового значення:

function handleClick() {
setAge(a => a + 1); // setAge(42 => 43)
setAge(a => a + 1); // setAge(43 => 44)
setAge(a => a + 1); // setAge(44 => 45)
}

Тут a => a + 1 — це ваша функція-оновлювач. Вона приймає поточне значення стану та обчислює з нього наступне значення.

React ставить усі функції-оновлювачі у чергу. Під час наступного рендеру він викличе їх у тому ж порядку:

  1. a => a + 1 отримає 42 як поточне значення та поверне 43.
  2. a => a + 1 отримає 43 як поточне значення та поверне 44.
  3. a => a + 1 отримає 44 як поточне значення та поверне 45.

Інших оновлень у черзі немає, тож React зрештою збереже 45 як поточне значення стану.

Зазвичай аргумент функції-оновлювача називають першою літерою назви змінної стану — наприклад, a для age. Однак ви також можете використати назви типу prevAge або будь-які інші, які вам зрозуміліші.

У режимі розробки React може викликати функції-оновлювачі двічі, щоб переконатися, що вони чисті.

Занурення

Чи завжди варто використовувати функцію-оновлювач?

Ви могли чути пораду завжди писати код у стилі setAge(a => a + 1), якщо новий стан обчислюється на основі попереднього. Це не завдає шкоди, але й не завжди є необхідним.

У більшості випадків немає різниці між цими двома підходами. React завжди гарантує, що для свідомих дій користувача, як-от кліки, значення змінної age буде оновлено до наступного кліку. Це означає, що обробник кліку не побачить “застаріле” значення age на початку виконання.

Однак якщо ви виконуєте кілька оновлень у межах однієї події, функції-оновлювачі можуть бути корисними. Вони також зручні, коли доступ до змінної стану ускладнений (з цим можна зіткнутися під час оптимізації повторного рендеру).

Якщо для вас важлива послідовність навіть за ціною трохи багатослівнішого синтаксису, цілком логічно завжди використовувати функцію-оновлювач, коли новий стан залежить від попереднього. Якщо ж нове значення залежить від попереднього стану іншої змінної, можливо, варто об’єднати їх в один об’єкт і використати редюсер.

Різниця між передаванням функції-оновлювача та безпосереднього значення

Приклад 1 із 2:
Передавання функції-оновлювача

У цьому прикладі передається функція-оновлювач, тож кнопка “+3” працює як очікується.

import { useState } from 'react';

export default function Counter() {
  const [age, setAge] = useState(42);

  function increment() {
    setAge(a => a + 1);
  }

  return (
    <>
      <h1>Ваш вік: {age}</h1>
      <button onClick={() => {
        increment();
        increment();
        increment();
      }}>+3</button>
      <button onClick={() => {
        increment();
      }}>+1</button>
    </>
  );
}


Оновлення стану, що містить об’єкти та масиви

У стан можна поміщати об’єкти та масиви. У React стан доступний лише для читання, тому його слід замінювати, а не змінювати наявні об’єкти. Наприклад, якщо у вас є об’єкт form у стані, не змінюйте його ось так:

// 🚩 Не змінюйте об'єкт у стані безпосередньо:
form.firstName = 'Тейлор';

Замість цього замініть весь об’єкт, створивши новий:

// ✅ Замініть стан новим об'єктом
setForm({
...form,
firstName: 'Тейлор'
});

Про це докладніше у розділах оновлення об’єктів у стані та оновлення масивів у стані..

Приклади об'єктів і масивів у стані

Приклад 1 із 4:
Форма (об’єкт)

У цьому прикладі змінна стану form зберігає об’єкт. Кожне поле вводу має обробник зміни, який викликає setForm із новим станом усієї форми. Синтаксис розпакування { ...form } гарантує, що об’єкт стану буде замінено, а не змінено напряму.

import { useState } from 'react';

export default function Form() {
  const [form, setForm] = useState({
    firstName: 'Барбара',
    lastName: 'Гепворт',
    email: 'bhepworth@sculpture.com',
  });

  return (
    <>
      <label>
        Ім’я:
        <input
          value={form.firstName}
          onChange={e => {
            setForm({
              ...form,
              firstName: e.target.value
            });
          }}
        />
      </label>
      <label>
        Прізвище:
        <input
          value={form.lastName}
          onChange={e => {
            setForm({
              ...form,
              lastName: e.target.value
            });
          }}
        />
      </label>
      <label>
        Email:
        <input
          value={form.email}
          onChange={e => {
            setForm({
              ...form,
              email: e.target.value
            });
          }}
        />
      </label>
      <p>
        {form.firstName}{' '}
        {form.lastName}{' '}
        ({form.email})
      </p>
    </>
  );
}


Уникайте повторного створення початкового стану

React зберігає початковий стан один раз і ігнорує його під час наступних рендерів.

function TodoList() {
const [todos, setTodos] = useState(createInitialTodos());
// ...

Хоча результат createInitialTodos() використовується лише під час першого рендеру, ви все одно викликаєте цю функцію на кожному рендері. Це може бути неефективно, якщо функція створює великі масиви або виконує ресурсоємні обчислення.

Щоб уникнути цього, у useState передайте її як функцію-ініціалізатор:

function TodoList() {
const [todos, setTodos] = useState(createInitialTodos);
// ...

Зверніть увагу, що ви передаєте createInitialTodos, тобто саму функцію, а не createInitialTodos(), що є результатом її виклику. Якщо ви передаєте функцію в useState, React викликає її лише під час ініціалізації.

У режимі розробки React може двічі викликати ваші ініціалізатори, щоб переконатися, що вони чисті.

Різниця між передаванням ініціалізатора і передаванням початкового стану напряму

Приклад 1 із 2:
Передавання функції-ініціалізатора

У цьому прикладі передається функція-ініціалізатор, тому createInitialTodos виконується лише під час ініціалізації. Вона не виконується під час повторних рендерів компонента, наприклад, коли ви щось вводите в поле.

import { useState } from 'react';

function createInitialTodos() {
  const initialTodos = [];
  for (let i = 0; i < 50; i++) {
    initialTodos.push({
      id: i,
      text: 'Елемент ' + (i + 1)
    });
  }
  return initialTodos;
}

export default function TodoList() {
  const [todos, setTodos] = useState(createInitialTodos);
  const [text, setText] = useState('');

  return (
    <>
      <input
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <button onClick={() => {
        setText('');
        setTodos([{
          id: todos.length,
          text: text
        }, ...todos]);
      }}>Додати</button>
      <ul>
        {todos.map(item => (
          <li key={item.id}>
            {item.text}
          </li>
        ))}
      </ul>
    </>
  );
}


Скидання стану за допомогою key

Ви часто зустрічатимете атрибут key при рендерінгу списків. Але він має ще одну цікаву властивість.

Ви можете скинути стан компонента, передавши йому інше значення key. У цьому прикладі кнопка Reset змінює змінну стану version, яку ми передаємо як key компоненту Form. Коли значення key змінюється, React створює компонент Form (і всіх його нащадків) заново, тож його стан скидається.

Докладніше читайте в розділі збереження і скидання стану.

import { useState } from 'react';

export default function App() {
  const [version, setVersion] = useState(0);

  function handleReset() {
    setVersion(version + 1);
  }

  return (
    <>
      <button onClick={handleReset}>Скинути</button>
      <Form key={version} />
    </>
  );
}

function Form() {
  const [name, setName] = useState('Тейлор');

  return (
    <>
      <input
        value={name}
        onChange={e => setName(e.target.value)}
      />
      <p>Привіт, {name}.</p>
    </>
  );
}


Збереження інформації з попередніх рендерів

Зазвичай ви оновлюєте стан у обробниках подій. Але іноді виникає потреба змінити стан у відповідь на сам рендер — наприклад, змінити стан, коли змінюється пропс.

У більшості випадків це не потрібно:

У рідкісних випадках, коли жодне з цих рішень не підходить, можна скористатися шаблоном, у якому функція set викликається під час рендеру, аби оновити стан на основі значень, які вже були відрендерені.

Ось приклад. Компонент CountLabel показує проп count, який йому передано:

export default function CountLabel({ count }) {
return <h1>{count}</h1>
}

Скажімо, ви хочете показувати, чи значення лічильника зросло чи зменшилося з моменту останньої зміни. Сам по собі count цього не показує — потрібно зберігати його попереднє значення. Додайте змінну стану prevCount, щоб відстежувати його. Додайте також trend, яка зберігатиме, зростає значення чи зменшується. Порівняйте prevCount і count, і якщо вони не рівні — оновіть обидва. Тепер ви можете показати не лише поточне значення, але й як воно змінилось.

import { useState } from 'react';

export default function CountLabel({ count }) {
  const [prevCount, setPrevCount] = useState(count);
  const [trend, setTrend] = useState(null);
  if (prevCount !== count) {
    setPrevCount(count);
    setTrend(count > prevCount ? 'зростає' : 'спадає');
  }
  return (
    <>
      <h1>{count}</h1>
      {trend && <p>Значення {trend}</p>}
    </>
  );
}

Зверніть увагу: якщо ви викликаєте set під час рендеру, це має бути всередині умови на кшталт prevCount !== count, і в тій же умові має бути виклик setPrevCount(count). Інакше компонент зациклиться і зламається. Крім того, так можна оновлювати стан лише поточного компонента. Виклик set в іншому компоненті під час рендеру — помилка. Нарешті, виклик set усе одно має оновлювати стан без мутацій, також це не дозвіл порушувати інші правила чистих функцій.

Цей патерн досить складний для розуміння і зазвичай краще його уникати. Але він кращий, ніж оновлення стану в ефекті. Коли ви викликаєте set під час рендеру, React виконає повторний рендер компонента одразу після return, до того, як почне рендерити дочірні. Тобто, дочірні компоненти не рендеряться двічі. Решта вашої функції-компонента все одно виконається (але її результат буде відкинуто). Якщо умова виклику set йде після всіх хуків — можна зробити ранній return;, аби почати рендер раніше.


Поширені проблеми

Я оновив стан, але в консолі виводиться старе значення

Виклик функції set не змінює стан у виконуваному коді:

function handleClick() {
console.log(count); // 0

setCount(count + 1); // Запит на ререндер із 1
console.log(count); // Все ще 0!

setTimeout(() => {
console.log(count); // Також 0!
}, 5000);
}

Це тому, що стан поводиться як знімок (snapshot). Оновлення стану — це запит на новий рендер із новим значенням стану, але воно не змінює змінну count у вже виконуваному обробнику подій.

Якщо вам потрібно використати наступний стан — збережіть його в змінну перед переданням у set:

const nextCount = count + 1;
setCount(nextCount);

console.log(count); // 0
console.log(nextCount); // 1

Я оновив стан, але інтерфейс не оновлюється

React ігноруватиме ваше оновлення, якщо новий стан ідентичний попередньому, згідно з порівнянням за Object.is. Це зазвичай трапляється, коли ви напряму змінюєте об’єкт або масив у стані:

obj.x = 10; // 🚩 Помилка: мутація існуючого об’єкта
setObj(obj); // 🚩 Не спрацює

Ви змінили існуючий об’єкт obj і знову передали його у setObj, тому React проігнорував оновлення. Щоб це виправити, потрібно завжди замінювати об’єкти й масиви у стані замість того, щоб мутувати їх:

// ✅ Правильно: створення нового об’єкта
setObj({
...obj,
x: 10
});

Я отримую помилку: “Too many re-renders”

Ви можете побачити помилку: Too many re-renders. React limits the number of renders to prevent an infinite loop. Зазвичай це означає, що ви безумовно викликаєте оновлення стану під час рендеру, тому компонент входить у цикл: рендер, оновлення стану (яке спричиняє рендер), рендер, оновлення стану і так далі. Дуже часто це трапляється через помилку в написанні обробника подій:

// 🚩 Помилка: обробник викликається під час рендеру
return <button onClick={handleClick()}>Натисни мене</button>

// ✅ Правильно: передається посилання на обробник
return <button onClick={handleClick}>Натисни мене</button>

// ✅ Правильно: передається стрілочна функція
return <button onClick={(e) => handleClick(e)}>Натисни мене</button>

Якщо не можете знайти причину цієї помилки, натисніть стрілку поруч із повідомленням у консолі та перегляньте стек викликів JavaScript, щоб знайти конкретний виклик функції set, що спричиняє помилку.


Моя функція-ініціалізатор або функція-оновлювач виконується двічі

У режимі Strict Mode React викликає деякі з ваших функцій двічі замість одного разу:

function TodoList() {
// Ця функція-компонент виконуватиметься двічі під час кожного рендеру.

const [todos, setTodos] = useState(() => {
// Ця функція-ініціалізатор буде викликана двічі під час ініціалізації.
return createTodos();
});

function handleClick() {
setTodos(prevTodos => {
// Ця функція-оновлювач буде викликана двічі при кожному кліку.
return [...prevTodos, createTodo()];
});
}
// ...

Це очікувана поведінка і вона не повинна порушити логіку вашого коду.

Це лише для розробки і допомагає зберігати компоненти чистими. React використовує результат одного з викликів і ігнорує результат іншого. Якщо ваш компонент, ініціалізатор і функція-оновлювач є чистими, ця поведінка не повинна вплинути на логіку. Якщо ж вони мають побічні ефекти — це допоможе виявити помилки.

Наприклад, ось ця нечиста функція-оновлювач мутує масив у стані:

setTodos(prevTodos => {
// 🚩 Помилка: мутація стану
prevTodos.push(createTodo());
});

Оскільки React викликає вашу функцію-оновлювач двічі, ви побачите, що todo додано двічі — це сигналізує про помилку. У цьому випадку можна виправити це замінюючи масив новим, а не мутуючи його:

setTodos(prevTodos => {
// ✅ Правильно: створення нового стану
return [...prevTodos, createTodo()];
});

Тепер, коли ця функція-оновлювач чиста, її повторний виклик не змінює поведінки. Саме тому подвійний виклик допомагає виявляти помилки. Лише функції компонента, ініціалізатора та оновлення повинні бути чистими. Обробники подій не повинні бути чистими, тож React ніколи не викликатиме їх двічі.

Дізнайтесь більше з розділу про чистоту компонентів.


Я намагаюся зберегти у стан функцію, але вона викликається замість цього

Не можна зберігати функцію у стан ось так:

const [fn, setFn] = useState(someFunction);

function handleClick() {
setFn(someOtherFunction);
}

Оскільки ви передаєте функцію, React припускає, що someFunction — це функція-ініціалізатор, а someOtherFunctionфункція-оновлювач, тому намагається викликати їх і зберегти результат. Щоб насправді зберегти функцію, потрібно додати () => перед ними в обох випадках. Тоді React збереже передані вами функції.

const [fn, setFn] = useState(() => someFunction);

function handleClick() {
setFn(() => someOtherFunction);
}