useEffect це хук, який дозволяє синхронізувати компонент із зовнішньою системою

useEffect(setup, dependencies?)

Короткий огляд

useEffect(setup, dependencies?)

Щоб оголосити Ефект, на верхньому рівні компонента викличте useEffect.

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);
// ...
}

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

Параметри

  • setup (з англ. встановлюючий): Функція з логікою вашого Ефекту. Ваша функція setup може додатково повертати cleanup функцію (з англ. cleanup - прибирання). Після того, як ваш компонент буде додано в DOM, React виконає функцію setup. Потім після кожного наступного рендеру React буде перевіряти чи змінилось значення хоча б однієї із залежностей, і якщо значення хоча б однієї залежності змінилось, то React спочатку виконає cleanup-функцію (якщо ви її надали) зі старими значеннями, а потім знову виконає setup функцію із новими значеннями. Після видалення компонента з DOM React виконає cleanup-функцію.

  • необов’язковий dependencies (з англ. залежності): Список реактивних значень, від яких залежить код всередині setup-функції. Під реактивними значеннями мається на увазі props, state, а також усі змінні та функції, оголошені безпосередньо в тілі вашого компонента. Якщо ваш лінтер налаштовано для React, він буде контролювати, що ви не забули додати до списку жодне з необхідних реактивних значень. Список залежностей має містити фіксовану кількість елементів і бути поданий підряд, наприклад [dep1, dep2, dep3]. React порівнюватиме кожну залежність із її попереднім значенням, використовуючи порівняння Object.is. Якщо ви не передасте масив залежностей, то ваш Effect виконуватиметься після додавання компонента в DOM та після кожного повторного рендеру компонента. І якщо передасте порожній масив, то ваш Effect виконається лише один раз після додавання компонента в DOM. Подивитись різницю між передачею масиву залежностей, порожнього масиву та не передаванням нічого.

Результат

useEffect повертає undefined.

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

  • useEffect — це Hook, тому його можна викликати лише на верхньому рівні вашого компонента або у власних Hook’ах. Не можна викликати його всередині циклів чи умов. Якщо вам потрібно використати його в циклах або умовах, то виділіть новий компонент, додайте useEffect в ньому і додавайте той компонент по циклу / за умовою.

  • Якщо ви не намагаєтеся синхронізуватися із зовнішньою системою, вам, ймовірно, не потрібен Effect.

  • Коли ввімкнено Strict Mode (з англ. Strict Mode - суворий режим), React запускатиме setup+cleanup один зайвий раз перед першим запуском setup ((але тільки в режимі розробки)). Це стрес-тест, який гарантує, що ваша логіка cleanup «відзеркалює» логіку setup і зупиняє або скасовує все, що робить setup. Якщо Strict Mode викликає проблему, реалізуйте cleanup-функцію.

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

  • Якщо ваш Effect не був спричинений взаємодією (наприклад кліком), React зазвичай дозволяє браузеру спочатку відмалювати оновлений екран, а потім виконати Effect. Якщо Effect робить щось візуальне (наприклад, зображає підказку), і затримка помітна (наприклад, блимає), замініть useEffect на useLayoutEffect.

  • Якщо ваш Effect викликаний взаємодією (наприклад, кліком), React може виконати ваш Effect до того, як браузер відмалює оновлений екран. Це гарантує, що результат Effect буде доступний для системи обробки подій. Зазвичай це очікуваний результат. Однак, якщо потрібно відкласти виконання на після малювання (наприклад, alert()), можна використати setTimeout. Див. reactwg/react-18/128 щодо деталей.

  • Навіть якщо ваш Effect спричинений взаємодією (наприклад, кліком), React може дозволити браузеру перемалювати екран до обробки оновлень стану всередині Ефекту. Зазвичай така поведінка працює добре. Але якщо потрібно заблокувати перемалювання, замініть useEffect на useLayoutEffect.

  • Ефекти виконуються лише на клієнті. Вони не запускаються під час серверного рендерингу.


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

З’єднання із зовнішньою системою

Деякі компоненти мають залишатися підключеними до мережі, до деякого API браузера або до сторонньої бібліотеки, доки вони відображаються на сторінці. Ці системи не контролюються React, тому вони називаються зовнішніми.

Щоб підключити ваш компонент до зовнішньої системи, викличте useEffect на верхньому рівні вашого компонента:

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);
// ...
}

Ви маєте передати два аргументи до useEffect:

  1. Функцію установки з кодом установки що підключається до цієї системи.
    • Він має повернути функцію очистки з кодом очистки який відключається від тієї системи.
  2. Масив залежностей, куди маєте передати всі значення від вашого компонента, які використовуються всередині тих функцій (функції установки та функції очистки).

React викликає ваші функцію установки та та функцію очистки кожен раз, коли це необхідно, а це може ставатись багато разів:

  1. Ваш код установки запускається коли ваш компонента додається на сторінку (монтується).
  2. Після кожного ререндеру, під час якого значення якоїсь із залежностей було змінено :
    • Спершу ваш код очистки запускається з старими пропсами і станом.
    • Тоді, ваш код установки запускається з новими пропсами та станом.
  3. Ваш код очистки виконується ще один, останній раз, коли ваш компонент видаляється з сторінки.

Давайте опишемо цю чергу використовуючи приклад вище

Коли компонент ChatRoom додається на сторінку, він підключається до чату використовуючи первинні значення serverUrl та roomId. Якщо хоча б одне із цих значень, будь то serverUrl чи roomId мінється в результаті ререндеру (наприклад якщо користувач обрав інший чат з випадаючого списку), наш Ефект відключиться від попереднього чату, і підключиться до наступного чату. Після того, як компонент ChatRoom буде демонтовано з сторінки, наш Ефект відключиться від чату останній раз.

Щоб допомогти знайти баги, в Strict Mode (з англ. суворий режим) в режимі розробки React умисно зайвий раз запускає функцію установки та функцію очистки перед тим як запустити функцію установки. Цей стрес-тест допомагає вам або переконатись, що логіка вашого Ефекту реалізована правильно, або ж помітити баг ще на етапі розробки (якщо із-за цього “надлишкового” запуску функції установки та функції очистки ваш компонент працює не так, як він мав би). Функція очистки мусить зупиняти або відміняти все, що запустила функція установки. Показник того, що код працює правильно - код має працювати однаково добре незалежно від того чи було запущено функцію установки один раз, чи було запущено функцію установки, потім функцію очистки і знову функцію установки. Дивитись поширені рішення.

Намагайтесь писати кожен Ефект як окремий процес і мисліть про один цикл установки/очистки за раз. Не важливо чи ваш компонент монтується, чи оновлюється, чи демонтовується. Якщо ваша логіка очистки правильно “віддзеркалює” логіку установки, тоді ваш компонент працюватиме правильно незалежно від того, скільки разів викликано функції установки та очистки.

Примітка

Ефект дозволяє синхронізувати ваш компонент із деякою зовнішньою системою (наприклад, службою чату). Тут зовнішня система означає будь-яку частину коду, яка не контролюється з React, як-от:

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

Приклади підключення до зовнішньої системи

Приклад 1 із 5:
Підключення до чат-сервера

У цьому прикладі компонент ChatRoom використовує Effect для підтримки з’єднання із зовнішньою системою, визначеною у файлі chat.js. Натисніть “Open chat” (Відкрити чат), щоб компонент ChatRoom з’явився. Цей застосунок (sandbox) працює в режимі розробки, тому відбувається додатковий цикл підключення та відключення, як пояснено тут. Спробуйте змінити значення roomId та serverUrl за допомогою випадаючого списку та поля введення і подивіться, як Effect повторно підключається до чату. Натисніть “Close chat” (Закрити чат), щоб побачити, як Effect відключається востаннє.

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => {
      connection.disconnect();
    };
  }, [roomId, serverUrl]);

  return (
    <>
      <label>
        URL сервера:{' '}
        <input
          value={serverUrl}
          onChange={e => setServerUrl(e.target.value)}
        />
      </label>
      <h1>Ласкаво просимо до чату {roomId} !</h1>
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('загальне');
  const [show, setShow] = useState(false);
  return (
    <>
      <label>
        Оберіть чат:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="загальне">загальне</option>
          <option value="подорожі">подорожі</option>
          <option value="подорожі">музика</option>
        </select>
      </label>
      <button onClick={() => setShow(!show)}>
        {show ? 'Закрити чат' : 'Відкрити чат'}
      </button>
      {show && <hr />}
      {show && <ChatRoom roomId={roomId} />}
    </>
  );
}


Обгортання Ефектів у користувацькі хуки

Ефекти є “рятівним виходом”: використовуйте їх, коли вам потрібно “вийти за межі React” і коли для вашого випадку використання немає кращого вбудованого рішення. Якщо ви помічаєте, що вам часто доводиться вручну писати Ефекти, це зазвичай є ознакою того, що вам потрібно виділити деякі користувацькі хуки [/learn/reusing-logic-with-custom-hooks] для поширених дій, на які покладаються ваші компоненти.

Наприклад, цей власний хук useChatRoom “загортає” логіку вашого Ефекту за більш декларативним API:

function useChatRoom({ serverUrl, roomId }) {
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId, serverUrl]);
}

Після цього ви можете бачити це з будь-якого компонента:

function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

useChatRoom({
roomId: roomId,
serverUrl: serverUrl
});
// ...

В екосистемі React також доступно багато чудових користувацьких Хуків для будь-якої мети.

Дізнайтися більше про обгортання Ефектів у користувацькі Хуки.

Приклади обгортання Ефектів у користувацькі Хуки

Приклад 1 із 3:
Користувацький Хук useChatRoom

Цей приклад ідентичний одному з попередніх прикладів, але логіка винесена в користувацький Хук.

import { useState } from 'react';
import { useChatRoom } from './useChatRoom.js';

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

  useChatRoom({
    roomId: roomId,
    serverUrl: serverUrl
  });

  return (
    <>
      <label>
        URL сервера:{' '}
        <input
          value={serverUrl}
          onChange={e => setServerUrl(e.target.value)}
        />
      </label>
      <h1>Ласкаво просимо до чату {roomId} !</h1>
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('загальне');
  const [show, setShow] = useState(false);
  return (
    <>
      <label>
        Оберіть чат:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="загальне">загальне</option>
          <option value="подорожі">подорожі</option>
          <option value="музика">музика</option>
        </select>
      </label>
      <button onClick={() => setShow(!show)}>
        {show ? 'Закрити чат' : 'Відкрити чат'}
      </button>
      {show && <hr />}
      {show && <ChatRoom roomId={roomId} />}
    </>
  );
}


Керування не-React віджетом

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

Наприклад, якщо у вас є сторонній віджет карти або компонент відеопрогравача, написаний без React, ви можете використати Ефект, щоб викликати його методи, які змусять його стан відповідати поточному стану вашого React-компонента. Цей Ефект створює екземпляр класу MapWidget, визначеного у map-widget.js. Коли ви змінюєте проп zoomLevel компонента Map, Ефект викликає setZoom() на екземплярі класу, щоб той був завжди синхронізованим:

import { useRef, useEffect } from 'react';
import { MapWidget } from './map-widget.js';

export default function Map({ zoomLevel }) {
  const containerRef = useRef(null);
  const mapRef = useRef(null);

  useEffect(() => {
    if (mapRef.current === null) {
      mapRef.current = new MapWidget(containerRef.current);
    }

    const map = mapRef.current;
    map.setZoom(zoomLevel);
  }, [zoomLevel]);

  return (
    <div
      style={{ width: 200, height: 200 }}
      ref={containerRef}
    />
  );
}

У цьому прикладі функція очистки не потрібна, оскільки клас MapWidget керує лише DOM-вузлом, який був йому переданий. Після того, як реактівський компонента Map буде видалено з документа, то обидва, і DOM-вузол, і екземпляр класу MapWidget будуть автоматично прибрані збирачем сміття (garbage-collector) браузерного рушія JavaScript.


Отримання даних за допомогою Ефектів

Ви можете використовувати Ефект (Effect) для отримання даних для вашого компонента. Зауважте, що якщо ви використовуєте фреймворк, використання механізму отримання даних вашого фреймворку буде набагато ефективнішим, ніж написання Ефектів вручну.

Якщо ви хочете отримати дані з Ефекту вручну, ваш код може виглядати так:

import { useState, useEffect } from 'react';
import { fetchBio } from './api.js';

export default function Page() {
const [person, setPerson] = useState('Руслан');
const [bio, setBio] = useState(null);

useEffect(() => {
let ignore = false;
setBio(null);
fetchBio(person).then(result => {
if (!ignore) {
setBio(result);
}
});
return () => {
ignore = true;
};
}, [person]);

// ...

Зверніть увагу на змінну ignore, яка спершу має значення false і потім змінюється на true під час очищення. Це гарантує, що ваш код не постраждає від “станів гонки” (race conditions): мережеві відповіді можуть надходити в іншому порядку, ніж ви їх відправили.

import { useState, useEffect } from 'react';
import { fetchBio } from './api.js';

export default function Page() {
  const [person, setPerson] = useState('Руслан');
  const [bio, setBio] = useState(null);
  useEffect(() => {
    let ignore = false;
    setBio(null);
    fetchBio(person).then(result => {
      if (!ignore) {
        setBio(result);
      }
    });
    return () => {
      ignore = true;
    }
  }, [person]);

  return (
    <>
      <select value={person} onChange={e => {
        setPerson(e.target.value);
      }}>
        <option value="Руслан">Руслан</option>
        <option value="Борис">Борис</option>
        <option value="Барбарис">Барбарис</option>
      </select>
      <hr />
      <p><i>{bio ?? 'Завантажуємо...'}</i></p>
    </>
  );
}

Ви також можете переписати це, використовуючи синтаксис async / await, але всеодно потрібно надати функцію очищення:

import { useState, useEffect } from 'react';
import { fetchBio } from './api.js';

export default function Page() {
  const [person, setPerson] = useState('Руслан');
  const [bio, setBio] = useState(null);
  useEffect(() => {
    async function startFetching() {
      setBio(null);
      const result = await fetchBio(person);
      if (!ignore) {
        setBio(result);
      }
    }

    let ignore = false;
    startFetching();
    return () => {
      ignore = true;
    }
  }, [person]);

  return (
    <>
      <select value={person} onChange={e => {
        setPerson(e.target.value);
      }}>
        <option value="Руслан">Руслан</option>
        <option value="Борис">Борис</option>
        <option value="Барбарис">Барбарис</option>
      </select>
      <hr />
      <p><i>{bio ?? 'Завантажуємо...'}</i></p>
    </>
  );
}

Написання логіки отримання даних безпосередньо в Ефектах призводить до повторюваного коду та ускладнює додавання оптимізацій, як-от кешування та рендеринг на стороні сервера, згодом. Простіше використовувати якийсь із користувацьких хуків (Custom Hook) – або свій власний, або той, що підтримується спільнотою.

Занурення

Які є хороші альтернативи отриманню даних в Ефектах?

Написання викликів fetch всередині Ефектів — це популярний спосіб отримання даних, особливо в додатках, які повністю працюють на стороні клієнта. Однак, це дуже ручний підхід, який має суттєві недоліки:

  • Ефекти не запускаються на сервері. Це означає, що початковий HTML, відрендерений сервером, буде містити лише стан завантаження, але без даних. Клієнтський комп’ютер повинен буде завантажити весь JavaScript і відрендерити ваш додаток, лише щоб виявити, що тепер йому потрібно завантажити дані. Це не дуже ефективно.
  • Отримання даних безпосередньо в Ефектах полегшує створення “мережевих водоспадів” (network waterfalls). Ви рендерите батьківський компонент, він отримує деякі дані, рендерить дочірні компоненти, а потім вони починають отримувати свої дані. Якщо мережа не дуже швидка, це значно повільніше, ніж отримання всіх даних паралельно.
  • Отримання даних безпосередньо в Ефектах зазвичай означає, що ви не завантажуєте попередньо і не кешуєте дані. Наприклад, якщо компонент демонтується, а потім монтується знову, йому доведеться знову отримувати дані.
  • Це не дуже ергономічно. Щоб написати виклики fetch так, щоб уникнути багів (наприклад таких, як стани гонки (race conditions).) - треба писати дуже багато шаблонного коду.

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

  • Якщо ви використовуєте фреймворк, використовуйте його вбудований механізм отримання даних. Сучасні фреймворки React мають інтегровані механізми отримання даних, які є ефективними і не страждають від вищезгаданих недоліків.
  • В іншому випадку, розгляньте можливість використання або створення клієнтського кешу. Популярні рішення з відкритим вихідним кодом включають React Query, useSWR та React Router 6.4+. Ви також можете створити власне рішення, і в такому випадку використовувати Ефекти “під капотом”, але також додати логіку для дедуплікації запитів, кешування відповідей та уникнення мережевих водоспадів (шляхом попереднього завантаження даних або піднесення вимог до даних до маршрутів).

Ви можете й далі отримувати дані безпосередньо в Ефектах, якщо жоден з цих підходів вам не підходить.


Зазначення реактивних залежностей

Зверніть увагу, що ви не можете “вибирати” залежності вашого Ефекту. Кожне реактивне значення, яке використовується кодом вашого Ефекту, має бути оголошено як залежність. Список залежностей вашого Ефекту визначається кодом, який оточує Ефект:

function ChatRoom({ roomId }) { // Це реактивне значення
const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // Це також реактивне значення

useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Цей Ефект читає ці реактивні значення
connection.connect();
return () => connection.disconnect();
}, [serverUrl, roomId]); // ✅ Тож ви повинні вказати їх як залежності вашого Ефекту
// ...
}

Якщо serverUrl або roomId зміняться, ваш Ефект повторно підключиться до чату, використовуючи нові значення.

Реактивними значеннями є як пропси (props), так і всі змінні й функції, оголошені безпосередньо всередині вашого компонента. Оскільки roomId та serverUrl є реактивними значеннями, ви не можете видалити їх із залежностей. Якщо ви спробуєте їх проігнорувати і ваш лінтер правильно налаштований для React, лінтер позначить це як помилку, яку вам потрібно виправити:

function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []); // 🔴 React Hook useEffect має пропущені залежності: 'roomId' і 'serverUrl'
// ...
}

Щоб видалити залежність, вам потрібно “довести” лінтеру, що вона не має бути залежністю. Наприклад, ви можете винести serverUrl за межі вашого компонента, щоб довести, що це не реактивне значення і не зміниться при повторних рендерах:

const serverUrl = 'https://localhost:1234'; // Більше не реактивне значення

function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ Всі залежності оголошено
// ...
}

Тепер, коли serverUrl більше не є реактивним значенням (і не може змінитися при повторному рендері), його можна не передавати в список залежностей. Якщо код вашого Ефекту не використовує жодних реактивних значень, його список залежностей має бути порожнім ([]):

const serverUrl = 'https://localhost:1234'; // Більше не реактивне значення
const roomId = 'music'; // Більше не реактивне значення

function ChatRoom() {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []); // ✅ Всі залежності оголошено
// ...
}

Ефект з порожніми залежностями не запускається повторно, навіть якщо змінюється якийсь з пропсів або змінних стану вашого компонента.

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

Якщо у вас є існуюча кодова база, у вас можуть бути деякі Ефекти, які ігнорують лінтер наступним чином:

useEffect(() => {
// ...
// 🔴 Уникайте ігнорування лінтера таким чином:
// eslint-ignore-next-line react-hooks/exhaustive-deps
}, []);

Коли залежності не відповідають коду, існує високий ризик появи помилок. Ігноруючи лінтер, ви “брешете” React про значення, від яких залежить ваш Ефект. Замість цього, доведіть, що вони непотрібні.

Приклади передачі реактивних залежностей

Приклад 1 із 3:
Передача масиву залежностей

Якщо ви вказуєте залежності, ваш Ефект запускається після початкового рендера і після повторних рендерів зі зміненими залежностями.

useEffect(() => {
// ...
}, [a, b]); // Запускається знову, якщо a або b відрізняються

У наведеному нижче прикладі serverUrl і roomId є реактивними значеннями, тому обидва повинні бути вказані як залежності. В результаті, вибір іншої кімнати у спадному списку або редагування вводу URL-адреси сервера призводить до повторного підключення чату. Однак, оскільки message не використовується в Ефекті (і, отже, не є залежністю), редагування повідомлення не призводить до повторного підключення до чату.

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');
  const [message, setMessage] = useState('');

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => {
      connection.disconnect();
    };
  }, [serverUrl, roomId]);

  return (
    <>
      <label>
        URL-адреса сервера:{' '}
        <input
          value={serverUrl}
          onChange={e => setServerUrl(e.target.value)}
        />
      </label>
      <h1>Ласкаво просимо до чату {roomId}!</h1>
      <label>
        Ваше повідомлення:{' '}
        <input value={message} onChange={e => setMessage(e.target.value)} />
      </label>
    </>
  );
}

export default function App() {
  const [show, setShow] = useState(false);
  const [roomId, setRoomId] = useState('загальне');
  return (
    <>
      <label>
        Оберіть кімнату чату:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="загальне">загальне</option>
          <option value="подорожі">подорожі</option>
          <option value="музика">музика</option>
        </select>
        <button onClick={() => setShow(!show)}>
          {show ? 'Закрити чат' : 'Відкрити чат'}
        </button>
      </label>
      {show && <hr />}
      {show && <ChatRoom roomId={roomId}/>}
    </>
  );
}


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

Коли ви хочете оновити стан на основі попереднього стану з Ефекту, ви можете зіткнутися з проблемою:

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

useEffect(() => {
const intervalId = setInterval(() => {
setCount(count + 1); // Ви хочете збільшувати лічильник щосекунди...
}, 1000)
return () => clearInterval(intervalId);
}, [count]); // 🚩 ... але вказування `count` як залежності завжди скидає інтервал.
// ...
}

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

Щоб виправити це, передайте функцію оновлення стану c => c + 1 до setCount:

import { useState, useEffect } from 'react';

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

  useEffect(() => {
    const intervalId = setInterval(() => {
      setCount(c => c + 1); // ✅ Передайте функцію оновлення стану
    }, 1000);
    return () => clearInterval(intervalId);
  }, []); // ✅ Тепер count не є залежністю

  return <h1>{count}</h1>;
}

Тепер, коли ви передаєте c => c + 1 замість count + 1, вашому Ефекту більше не потрібно залежати від count. Внаслідок цього виправлення, йому не доведеться очищати та налаштовувати інтервал знову щоразу, коли змінюється count.


Видалення непотрібних залежностей-об’єктів

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

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');

const options = { // 🚩 Цей об'єкт створюється з нуля при кожному повторному рендері
serverUrl: serverUrl,
roomId: roomId
};

useEffect(() => {
const connection = createConnection(options); // Він використовується всередині Ефекту
connection.connect();
return () => connection.disconnect();
}, [options]); // 🚩 В результаті, ці залежності завжди відрізняються при повторному рендері
// ...

Уникайте використання об’єкта, створеного під час рендерингу, як залежності. Замість цього, створіть об’єкт всередині Ефекту:

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  useEffect(() => {
    const options = {
      serverUrl: serverUrl,
      roomId: roomId
    };
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);

  return (
    <>
      <h1>Ласкаво просимо до кімнати {roomId}!</h1>
      <input value={message} onChange={e => setMessage(e.target.value)} />
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('загальне');
  return (
    <>
      <label>
        Оберіть кімнату:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="загальне">загальне</option>
          <option value="подорожі">подорожі</option>
          <option value="музика">музика</option>
        </select>
      </label>
      <hr />
      <ChatRoom roomId={roomId} />
    </>
  );
}

Тепер, коли ви створюєте об’єкт options всередині Ефекту, сам Ефект залежить лише від рядка roomId.

Завдяки цьому виправленню, введення тексту у поле вводу не призводить до повторного підключення чату. На відміну від об’єкта, який перестворюється, рядок, як наприклад roomId, не змінюється (звісно ж якщо ви не зміните його значення). Дізнайтеся більше про видалення залежностей.


Видалення непотрібних залежностей-функцій

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

function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');

function createOptions() { // 🚩 Ця функція створюється з нуля при кожному повторному рендері
return {
serverUrl: serverUrl,
roomId: roomId
};
}

useEffect(() => {
const options = createOptions(); // Вона використовується всередині Ефекту
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // 🚩 В результаті, ці залежності завжди відрізняються при повторному рендері
// ...

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

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

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  useEffect(() => {
    function createOptions() {
      return {
        serverUrl: serverUrl,
        roomId: roomId
      };
    }

    const options = createOptions();
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);

  return (
    <>
      <h1>Ласкаво просимо до чату {roomId}!</h1>
      <input value={message} onChange={e => setMessage(e.target.value)} />
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('загальне');
  return (
    <>
      <label>
        Оберіть кімнату чату:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="загальне">загальне</option>
          <option value="подорожі">подорожі</option>
          <option value="music">music</option>
        </select>
      </label>
      <hr />
      <ChatRoom roomId={roomId} />
    </>
  );
}

Тепер, коли ви визначаєте функцію createOptions всередині Ефекту, сам Ефект залежить лише від рядка roomId. Завдяки цьому виправленню, введення тексту у поле вводу не призводить до повторного підключення чату. На відміну від функції, яка перестворюється, рядок, як roomId, не змінюється, якщо ви не присвоїте йому інше значення. Дізнайтися більше про видалення залежностей.


Зчитування найновіших пропсів та стану з Ефекту

За замовчуванням, коли ви зчитуєте реактивне значення з Ефекту, ви повинні додати його як залежність. Це гарантує, що ваш Ефект “реагує” на кожну зміну цього значення. Для більшості залежностей це саме та поведінка, яку ви хочете.

Однак, іноді ви захочете зчитувати найновіші пропси та стан з Ефекту, не “реагуючи” на них. Наприклад, уявіть, що ви хочете реєструвати кількість товарів у кошику для кожного відвідування сторінки:

function Page({ url, shoppingCart }) {
useEffect(() => {
logVisit(url, shoppingCart.length);
}, [url, shoppingCart]); // ✅ Всі залежності оголошено
// ...
}

Що робити, якщо ви хочете реєструвати нове відвідування сторінки після кожної зміни url, але не якщо змінюється лише shoppingCart? Ви не можете виключити shoppingCart із залежностей, не порушивши [правил реактивності].(#specifying-reactive-dependencies) Однак ви можете вказати, що не хочете, щоб частина коду “реагувала” на зміни, навіть якщо вона викликається зсередини Ефекту. Оголосіть Подію Ефекту за допомогою Хука useEffectEvent і перемістіть код, що зчитує shoppingCart, всередину неї:

function Page({ url, shoppingCart }) {
const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, shoppingCart.length)
});

useEffect(() => {
onVisit(url);
}, [url]); // ✅ Всі залежності оголошено
// ...
}

Події Ефекту не є реактивними і завжди повинні бути виключені із залежностей вашого Ефекту. Саме це дозволяє вам розміщувати всередині них нереактивний код (де ви можете зчитувати найновіше значення деяких пропсів та стану). Зчитуючи shoppingCart всередині onVisit, ви гарантуєте, що shoppingCart не перезапустить ваш Ефект.

Дізнайтеся більше про те, як Події Ефекту дозволяють розділити реактивний і нереактивний код.


Відображення різного вмісту на сервері та клієнті

Якщо ваш додаток використовує рендеринг на стороні сервера (або напряму, або через фреймворк), ваш компонент буде рендеритися у двох різних середовищах. На сервері він буде рендеритися для створення початкового HTML. На клієнті React знову запустить код рендерингу, щоб він міг приєднати ваші обробники подій до цього HTML. Саме тому, щоб гідратація працювала, ваш початковий вивід рендерингу має бути ідентичним на клієнті та сервері.

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

function MyComponent() {
const [didMount, setDidMount] = useState(false);

useEffect(() => {
setDidMount(true);
}, []);

if (didMount) {
// ... повернути JSX, лише для клієнта ...
} else {
// ... повернути початковий JSX ...
}
}

Поки додаток завантажується, користувач побачить початковий вивід рендерингу. Потім, коли він завантажиться та гідратується, ваш Ефект запуститься та змінить значення didMount на true, викликаючи повторний рендер. Це переключить на вивід рендерингу, який був обчислений лише на клієнті. Ефекти не запускаються на сервері, тому didMount був false під час початкового рендерингу на сервері.

Використовуйте цей шаблон рідко. Майте на увазі, що користувачі з повільним з’єднанням бачитимуть початковий вміст протягом досить тривалого часу – потенційно, багато секунд – тому ви не хочете робити різких змін у зовнішньому вигляді вашого компонента. У багатьох випадках ви можете уникнути необхідності цього з допомогою CSS, показуючи різні речі за різних умов.


Вирішення проблем

Мій Ефект запускається двічі, коли компонент монтується

Коли ввімкнено Суворий режим, у розробці React запускає функції установки та очистки один додатковий раз перед головним запуском функції установки.

Це стрес-тест, який перевіряє, чи правильно реалізована логіка вашого Ефекту. Якщо це викликає видимі проблеми, ваша функція очищення не має деякої логіки. Функція очищення повинна зупиняти або скасовувати те, що робила функція установки. Основне правило полягає в тому, що користувач не повинен мати змоги відрізнити, чи викликається функція установки один раз (як у продакшені), чи в послідовності установка → очищення → установка (як у розробці).

Дізнайтеся більше про те, як це допомагає знайти помилки та як виправити вашу логіку.


Мій Ефект запускається після кожного повторного рендера

Спочатку перевірте, чи ви не забули вказати масив залежностей:

useEffect(() => {
// ...
}); // 🚩 Немає масиву залежностей: повторно запускається після кожного рендера!

Якщо ви вказали масив залежностей, але ваш Ефект все одно повторно запускається в циклі, це тому, що одна з ваших залежностей відрізняється при кожному повторному рендері.

Ви можете відлагодити цю проблему, вручну відобразивши ваші залежності в консолі:

useEffect(() => {
// ..
}, [serverUrl, roomId]);

console.log([serverUrl, roomId]);

Потім ви можете клацнути правою кнопкою миші на масивах з різних повторних рендерів у консолі та вибрати “Store as a global variable” (Зберегти як глобальну змінну) для обох із них. Припускаючи, що перший був збережений як temp1, а другий як temp2, ви можете використовувати консоль браузера, щоб перевірити, чи кожна залежність в обох масивах однакова:

Object.is(temp1[0], temp2[0]); // Чи перша залежність однакова між масивами?
Object.is(temp1[1], temp2[1]); // Чи друга залежність однакова між масивами?
Object.is(temp1[2], temp2[2]); // ... і так далі для кожної залежності ...

Коли ви знайдете залежність, яка відрізняється при кожному повторному рендері, ви зазвичай можете виправити її одним із цих способів:

Як крайній захід (якщо ці методи не допомогли), оберніть її створення за допомогою useMemo або useCallback (для функцій).


Мій Ефект продовжує повторно запускатися у нескінченному циклі

Якщо ваш Ефект запускається у нескінченному циклі, значить відбуваються дві речі:

  • Ваш Ефект оновлює якийсь стан.
  • Цей стан призводить до повторного рендера, що спричиняє зміну залежностей Ефекту.

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

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

Якщо ви дійсно синхронізуєтеся з якоюсь зовнішньою системою, подумайте, чому і за яких умов ваш Ефект повинен оновлювати стан. Чи змінилося щось, що впливає на візуальний вивід вашого компонента? Якщо вам потрібно відстежувати деякі дані, які не використовуються для рендерингу, більш доречним може бути реф (який не викликає повторних рендерів). Переконайтеся, що ваш Ефект не оновлює стан (і не викликає повторні рендери) більше, ніж потрібно.

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


Моя логіка очищення запускається, хоча мій компонент не розмонтовувався

Функція очистки запускається не лише під час розмонтування, але й перед кожним повторним рендером зі зміненими залежностями. Крім того, у розробці React запускає установки + очищення один додатковий раз одразу після монтування компонента.

Якщо у вас є код очищення без відповідного коду установки, це зазвичай є ознакою поганого коду:

useEffect(() => {
// 🔴 Уникайте: Логіка очищення без відповідної логіки установки
return () => {
doSomething();
};
}, []);

Ваша логіка очищення має бути “симетричною” до логіки установки та має зупиняти або скасовувати те, що робило налаштування:

useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);

Дізнайтися, чим життєвий цикл Ефекту відрізняється від життєвого циклу компонента.


Мій Ефект робить щось візуальне, і я бачу мерехтіння перед його запуском

Якщо ваш Ефект повинен блокувати відображення браузером екрана, замініть useEffect на useLayoutEffect. Зауважте, що це не повинно бути потрібним для переважної більшості Ефектів. Вам це знадобиться лише в тому випадку, якщо критично важливо запустити ваш Ефект до відображення браузером: наприклад, для вимірювання та позиціонування підказки, перш ніж користувач її побачить.