useEffect

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

useEffect(setup, dependencies?)

Довідник

useEffect(setup, dependencies?)

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

import { 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: Функція з логікою вашого ефекту. Ваша функція налаштування може також опціонально повертати функцію cleanup. Коли ваш компонент буде додано до DOM, React запустить вашу функцію налаштування. Після кожного повторного рендерингу зі зміненими залежностями React спочатку запустить функцію очищення (якщо ви її надали) зі старими значеннями, а потім запустить вашу функцію налаштування з новими значеннями. Після того, як ваш компонент буде видалено з DOM, React запустить вашу функцію очищення.

  • опції залежності: список усіх реактивних значень, на які посилаються у коді setup. Реактивні значення включають пропси, стан і всі змінні та функції, оголошені безпосередньо у тілі вашого компонента. Якщо ваш лінтер налаштований на React, він буде перевіряти, щоб кожне реактивне значення було правильно вказане як залежність. Список залежностей повинен мати постійну кількість елементів і бути записаний в рядок як [dep1, dep2, dep3]. React буде порівнювати кожну залежність з попереднім значенням за допомогою порівняння Object.is. Якщо ви опустите цей аргумент, ваш ефект буде повторно запускатися після кожного повторного рендерингу компонента. Подивіться різницю між передачею масиву залежностей, порожнього масиву та відсутності залежностей взагалі.

Повернення

useEffect повертає undefined.

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

  • useEffect є хуком, тому ви можете викликати його лише на верхньому рівні вашого компонента або ваших власних хуків. Ви не можете викликати його всередині циклів або умов. Якщо вам це потрібно, витягніть новий компонент і перемістіть стан до нього.

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

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

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

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

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

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


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

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

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

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

import { 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. Функція налаштування з кодом налаштування</CodeStep> , який підключається до цієї системи. <ul> <li>Вона повинна повертати функцію <em>очищення</em> з <CodeStep data-step="2">код очищення</CodeStep> який від'єднується від цієї системи.</li> </ul></li> <li>A <CodeStep data-step="3">список залежностей</CodeStep> включаючи кожне значення з вашого компонента, що використовується всередині цих функцій.</li></ol> <p><strong>React викликає ваші функції налаштування та очищення, коли це необхідно, що може траплятися кілька разів:</strong></p> <ol><li>Ваш <CodeStep data-step="1">код налаштування</CodeStep> запускається, коли ваш компонент додається на сторінку <em>(mounts)</em>.</li> <li>Після кожного повторного рендерингу вашого компонента, де <CodeStep data-step="3">залежності</CodeStep> змінилися: <ul> <li>По-перше, ваш <CodeStep data-step="2">код очищення</CodeStep> працює зі старими пропсами і станом.</li> <li>Тоді, ваш <CodeStep data-step="1">код налаштування</CodeStep> запускається з новими пропсами та станом.</li> </ul></li> <li>Ваш <CodeStep data-step="2">код очищення</CodeStep> виконується востаннє після того, як ваш компонент буде видалено зі сторінки <em>(демонтується).</em></li></ol> <p><strong>Проілюструємо цю послідовність для прикладу вище.</strong></p> <p>Коли компонент <code>ChatRoom, наведений вище, буде додано на сторінку, він з'єднається з кімнатою чату з початковими serverUrl та roomId. Якщо serverUrl або roomId зміняться в результаті повторного рендерингу (скажімо, якщо користувач вибере іншу кімнату чату у випадаючому списку), ваш ефект від'єднається від попередньої кімнати і підключиться до наступної. Коли компонент ChatRoom буде видалено зі сторінки, ваш ефект від'єднається востаннє.

    Щоб допомогти вам знаходити вади, в розробці React запускає setup</CodeStep> та <CodeStep data-step="2">очищення</CodeStep> ще один раз перед <CodeStep data-step="1">setup</CodeStep> .</strong> Це стрес-тест, який перевіряє, чи правильно реалізована логіка вашого ефекту. Якщо це викликає видимі проблеми, у вашій функції очищення бракує певної логіки. Функція очищення повинна зупинити або скасувати все, що робила функція налаштування. Емпіричне правило полягає в тому, що користувач не повинен мати змоги відрізнити однократний виклик setup (як у виробництві) від послідовності <em>setup</em> → <em>cleanup</em> → <em>setup</em> (як у розробці). <a href="/learn/synchronizing-with-effects#how-to-handle-the-effect-firing-twice-in-development">Дивіться типові рішення.</a></p> <p><strong>Спробуйте <a href="/learn/lifecycle-of-reactive-effects#each-effect-represent-a-separate-synchronization-process"> писати кожен ефект як незалежний процес</a> та <a href="/learn/lifecycle-of-reactive-effects#thinking-from-the-effects-perspective">>думати про один цикл налаштування/очищення за один раз. </a></strong> Не повинно мати значення, ваш компонент монтується, оновлюється або демонтується. Коли ваша логіка очищення правильно "віддзеркалює" логіку налаштування, ваш ефект буде стійким до запуску налаштування та очищення так часто, як це потрібно.</p> <Примітка><p>Ефект дозволяє вам <a href="/learn/synchronizing-with-effects">тримати ваш компонент синхронізованим</a> з деякою зовнішньою системою (наприклад, сервісом чату). Тут <em>зовнішня система</em> означає будь-який фрагмент коду, який не контролюється React, наприклад:</p> <ul> <li>Таймер, керований за допомогою <CodeStep data-step="1"><a href="https://developer.mozilla.org/en-US/docs/Web/API/setInterval" target="_blank" rel="nofollow noopener noreferrer"><code>setInterval() та <a href="https://developer.mozilla.org/en-US/docs/Web/API/clearInterval" target="_blank" rel="nofollow noopener noreferrer"><code>clearInterval() .

  2. Підписка на подію з використанням <a href="https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener" target="_blank" rel="nofollow noopener noreferrer"><><код>window.addEventListener() та <a href="https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener" target="_blank" rel="nofollow noopener noreferrer"><code>window.removeEventListener() .
  3. Стороння бібліотека анімації з API на зразок <code>animation.start() та <code>animation.reset() .
  4. Якщо ви не підключаєтеся до жодної зовнішньої системи, ймовірно, вам не потрібен ефект.

    Підключення до сервера чату

    У цьому прикладі компонент ChatRoom використовує ефект, щоб залишатися на зв'язку із зовнішньою системою, визначеною у chat.js. Натисніть кнопку "Відкрити чат", щоб з'явився компонент ChatRoom. Ця пісочниця працює у режимі розробки, тому у ній є додатковий цикл з'єднання та від'єднання, як описано тут. Спробуйте змінити roomId і serverUrl за допомогою випадаючого списку і введення, і подивіться, як ефект знову з'єднається з чатом. Натисніть "Закрити чат", щоб побачити, як ефект востаннє від'єднається від чату.

    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>
            Server URL:{' '}
            <input
              value={serverUrl}
              onChange={e => setServerUrl(e.target.value)}
            />
          </label>
          <h1>Welcome to the {roomId} room!</h1>
        </>
      );
    }
    
    export default function App() {
      const [roomId, setRoomId] = useState('general');
      const [show, setShow] = useState(false);
      return (
        <>
          <label>
            Choose the chat room:{' '}
            <select
              value={roomId}
              onChange={e => setRoomId(e.target.value)}
            >
              <option value="general">general</option>
              <option value="travel">travel</option>
              <option value="music">music</option>
            </select>
          </label>
          <button onClick={() => setShow(!show)}>
            {show ? 'Close chat' : 'Open chat'}
          </button>
          {show && <hr />}
          {show && <ChatRoom roomId={roomId} />}
        </>
      );
    }
    export function createConnection(serverUrl, roomId) {
      // A real implementation would actually connect to the server
      return {
        connect() {
          console.log('✅ Connecting to "' + roomId + '" room at ' + serverUrl + '...');
        },
        disconnect() {
          console.log('❌ Disconnected from "' + roomId + '" room at ' + serverUrl);
        }
      };
    }
    input { display: block; margin-bottom: 20px; }
    button { margin-left: 10px; }

    прослуховування глобальної події браузера

    У цьому прикладі зовнішньою системою є сам DOM браузера. Зазвичай ви вказуєте слухачів подій за допомогою JSX, але ви не можете слухати глобальний об'єкт window у такий спосіб. За допомогою ефекту можна підключитися до об'єкта window і прослуховувати його події. Прослуховування події pointermove дає змогу відстежувати положення курсору (або пальця) та оновлювати червону точку, щоб вона рухалася разом з ним.

    import { useState, useEffect } from 'react';
    
    export default function App() {
      const [position, setPosition] = useState({ x: 0, y: 0 });
    
      useEffect(() => {
        function handleMove(e) {
          setPosition({ x: e.clientX, y: e.clientY });
        }
        window.addEventListener('pointermove', handleMove);
        return () => {
          window.removeEventListener('pointermove', handleMove);
        };
      }, []);
    
      return (
        <div style={{
          position: 'absolute',
          backgroundColor: 'pink',
          borderRadius: '50%',
          opacity: 0.6,
          transform: `translate(${position.x}px, ${position.y}px)`,
          pointerEvents: 'none',
          left: -20,
          top: -20,
          width: 40,
          height: 40,
        }} />
      );
    }
    body {
      min-height: 300px;
    }

    Запуск анімації

    У цьому прикладі зовнішньою системою є бібліотека анімації у animation.js. Вона надає клас JavaScript з назвою FadeInAnimation, який приймає вузол DOM як аргумент і розкриває методи start() і stop() для керування анімацією. Цей компонент використовує посилання для доступу до базового DOM-вузла. Ефект зчитує DOM-вузол з рефа і автоматично запускає анімацію для цього вузла, коли з'являється компонент.

    import { useState, useEffect, useRef } from 'react';
    import { FadeInAnimation } from './animation.js';
    
    function Welcome() {
      const ref = useRef(null);
    
      useEffect(() => {
        const animation = new FadeInAnimation(ref.current);
        animation.start(1000);
        return () => {
          animation.stop();
        };
      }, []);
    
      return (
        <h1
          ref={ref}
          style={{
            opacity: 0,
            color: 'white',
            padding: 50,
            textAlign: 'center',
            fontSize: 50,
            backgroundImage: 'radial-gradient(circle, rgba(63,94,251,1) 0%, rgba(252,70,107,1) 100%)'
          }}
        >
          Welcome
        </h1>
      );
    }
    
    export default function App() {
      const [show, setShow] = useState(false);
      return (
        <>
          <button onClick={() => setShow(!show)}>
            {show ? 'Remove' : 'Show'}
          </button>
          <hr />
          {show && <Welcome />}
        </>
      );
    }
    export class FadeInAnimation {
      constructor(node) {
        this.node = node;
      }
      start(duration) {
        this.duration = duration;
        if (this.duration === 0) {
          // Jump to end immediately
          this.onProgress(1);
        } else {
          this.onProgress(0);
          // Start animating
          this.startTime = performance.now();
          this.frameId = requestAnimationFrame(() => this.onFrame());
        }
      }
      onFrame() {
        const timePassed = performance.now() - this.startTime;
        const progress = Math.min(timePassed / this.duration, 1);
        this.onProgress(progress);
        if (progress < 1) {
          // We still have more frames to paint
          this.frameId = requestAnimationFrame(() => this.onFrame());
        }
      }
      onProgress(progress) {
        this.node.style.opacity = progress;
      }
      stop() {
        cancelAnimationFrame(this.frameId);
        this.startTime = null;
        this.frameId = null;
        this.duration = 0;
      }
    }
    label, button { display: block; margin-bottom: 20px; }
    html, body { min-height: 300px; }

    Керування модальним діалогом

    У цьому прикладі зовнішньою системою є DOM браузера. Компонент ModalDialog рендерить елемент <dialog>. Він використовує ефект для синхронізації пропса isOpen з викликами методів showModal() та close().

    import { useState } from 'react';
    import ModalDialog from './ModalDialog.js';
    
    export default function App() {
      const [show, setShow] = useState(false);
      return (
        <>
          <button onClick={() => setShow(true)}>
            Open dialog
          </button>
          <ModalDialog isOpen={show}>
            Hello there!
            <br />
            <button onClick={() => {
              setShow(false);
            }}>Close</button>
          </ModalDialog>
        </>
      );
    }
    import { useEffect, useRef } from 'react';
    
    export default function ModalDialog({ isOpen, children }) {
      const ref = useRef();
    
      useEffect(() => {
        if (!isOpen) {
          return;
        }
        const dialog = ref.current;
        dialog.showModal();
        return () => {
          dialog.close();
        };
      }, [isOpen]);
    
      return <dialog ref={ref}>{children}</dialog>;
    }
    body {
      min-height: 300px;
    }

    Видимість елементів відстеження

    У цьому прикладі зовнішньою системою знову є DOM браузера. Компонент App показує довгий список, потім компонент Box, а потім ще один довгий список. Прокрутіть список вниз. Зверніть увагу, що коли весь компонент Box повністю видно у вікні перегляду, колір фону змінюється на чорний. Для цього компонент Box використовує ефект для керування IntersectionObserver. Цей API браузера сповіщає вас, коли DOM-елемент стає видимим у вікні перегляду.

    import Box from './Box.js';
    
    export default function App() {
      return (
        <>
          <LongSection />
          <Box />
          <LongSection />
          <Box />
          <LongSection />
        </>
      );
    }
    
    function LongSection() {
      const items = [];
      for (let i = 0; i < 50; i++) {
        items.push(<li key={i}>Item #{i} (keep scrolling)</li>);
      }
      return <ul>{items}</ul>
    }
    import { useRef, useEffect } from 'react';
    
    export default function Box() {
      const ref = useRef(null);
    
      useEffect(() => {
        const div = ref.current;
        const observer = new IntersectionObserver(entries => {
          const entry = entries[0];
          if (entry.isIntersecting) {
            document.body.style.backgroundColor = 'black';
            document.body.style.color = 'white';
          } else {
            document.body.style.backgroundColor = 'white';
            document.body.style.color = 'black';
          }
        }, {
           threshold: 1.0
        });
        observer.observe(div);
        return () => {
          observer.disconnect();
        }
      }, []);
    
      return (
        <div ref={ref} style={{
          margin: 20,
          height: 100,
          width: 100,
          border: '2px solid black',
          backgroundColor: 'blue'
        }} />
      );
    }

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

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

    Наприклад, цей 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 також є багато чудових кастомних хуків для будь-яких цілей.

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

    Користувацький 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>
            Server URL:{' '}
            <input
              value={serverUrl}
              onChange={e => setServerUrl(e.target.value)}
            />
          </label>
          <h1>Welcome to the {roomId} room!</h1>
        </>
      );
    }
    
    export default function App() {
      const [roomId, setRoomId] = useState('general');
      const [show, setShow] = useState(false);
      return (
        <>
          <label>
            Choose the chat room:{' '}
            <select
              value={roomId}
              onChange={e => setRoomId(e.target.value)}
            >
              <option value="general">general</option>
              <option value="travel">travel</option>
              <option value="music">music</option>
            </select>
          </label>
          <button onClick={() => setShow(!show)}>
            {show ? 'Close chat' : 'Open chat'}
          </button>
          {show && <hr />}
          {show && <ChatRoom roomId={roomId} />}
        </>
      );
    }
    import { useEffect } from 'react';
    import { createConnection } from './chat.js';
    
    export function useChatRoom({ serverUrl, roomId }) {
      useEffect(() => {
        const connection = createConnection(serverUrl, roomId);
        connection.connect();
        return () => {
          connection.disconnect();
        };
      }, [roomId, serverUrl]);
    }
    export function createConnection(serverUrl, roomId) {
      // A real implementation would actually connect to the server
      return {
        connect() {
          console.log('✅ Connecting to "' + roomId + '" room at ' + serverUrl + '...');
        },
        disconnect() {
          console.log('❌ Disconnected from "' + roomId + '" room at ' + serverUrl);
        }
      };
    }
    input { display: block; margin-bottom: 20px; }
    button { margin-left: 10px; }

    Митний useWindowListener Гачок

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

    import { useState } from 'react';
    import { useWindowListener } from './useWindowListener.js';
    
    export default function App() {
      const [position, setPosition] = useState({ x: 0, y: 0 });
    
      useWindowListener('pointermove', (e) => {
        setPosition({ x: e.clientX, y: e.clientY });
      });
    
      return (
        <div style={{
          position: 'absolute',
          backgroundColor: 'pink',
          borderRadius: '50%',
          opacity: 0.6,
          transform: `translate(${position.x}px, ${position.y}px)`,
          pointerEvents: 'none',
          left: -20,
          top: -20,
          width: 40,
          height: 40,
        }} />
      );
    }
    import { useState, useEffect } from 'react';
    
    export function useWindowListener(eventType, listener) {
      useEffect(() => {
        window.addEventListener(eventType, listener);
        return () => {
          window.removeEventListener(eventType, listener);
        };
      }, [eventType, listener]);
    }
    body {
      min-height: 300px;
    }

    Користувацький useIntersectionObserver хук

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

    import Box from './Box.js';
    
    export default function App() {
      return (
        <>
          <LongSection />
          <Box />
          <LongSection />
          <Box />
          <LongSection />
        </>
      );
    }
    
    function LongSection() {
      const items = [];
      for (let i = 0; i < 50; i++) {
        items.push(<li key={i}>Item #{i} (keep scrolling)</li>);
      }
      return <ul>{items}</ul>
    }
    import { useRef, useEffect } from 'react';
    import { useIntersectionObserver } from './useIntersectionObserver.js';
    
    export default function Box() {
      const ref = useRef(null);
      const isIntersecting = useIntersectionObserver(ref);
    
      useEffect(() => {
       if (isIntersecting) {
          document.body.style.backgroundColor = 'black';
          document.body.style.color = 'white';
        } else {
          document.body.style.backgroundColor = 'white';
          document.body.style.color = 'black';
        }
      }, [isIntersecting]);
    
      return (
        <div ref={ref} style={{
          margin: 20,
          height: 100,
          width: 100,
          border: '2px solid black',
          backgroundColor: 'blue'
        }} />
      );
    }
    import { useState, useEffect } from 'react';
    
    export function useIntersectionObserver(ref) {
      const [isIntersecting, setIsIntersecting] = useState(false);
    
      useEffect(() => {
        const div = ref.current;
        const observer = new IntersectionObserver(entries => {
          const entry = entries[0];
          setIsIntersecting(entry.isIntersecting);
        }, {
           threshold: 1.0
        });
        observer.observe(div);
        return () => {
          observer.disconnect();
        }
      }, [ref]);
    
      return isIntersecting;
    }

    Керування віджетом, що не належить до React

    .

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

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

    {
      "dependencies": {
        "leaflet": "1.9.1",
        "react": "latest",
        "react-dom": "latest",
        "react-scripts": "latest",
        "remarkable": "2.0.1"
      },
      "scripts": {
        "start": "react-scripts start",
        "build": "react-scripts build",
        "test": "react-scripts test --env=jsdom",
        "eject": "react-scripts eject"
      }
    }
    import { useState } from 'react';
    import Map from './Map.js';
    
    export default function App() {
      const [zoomLevel, setZoomLevel] = useState(0);
      return (
        <>
          Zoom level: {zoomLevel}x
          <button onClick={() => setZoomLevel(zoomLevel + 1)}>+</button>
          <button onClick={() => setZoomLevel(zoomLevel - 1)}>-</button>
          <hr />
          <Map zoomLevel={zoomLevel} />
        </>
      );
    }
    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}
        />
      );
    }
    import 'leaflet/dist/leaflet.css';
    import * as L from 'leaflet';
    
    export class MapWidget {
      constructor(domNode) {
        this.map = L.map(domNode, {
          zoomControl: false,
          doubleClickZoom: false,
          boxZoom: false,
          keyboard: false,
          scrollWheelZoom: false,
          zoomAnimation: false,
          touchZoom: false,
          zoomSnap: 0.1
        });
        L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
          maxZoom: 19,
          attribution: '© OpenStreetMap'
        }).addTo(this.map);
        this.map.setView([0, 0], 0);
      }
      setZoom(level) {
        this.map.setZoom(level);
      }
    }
    button { margin: 5px; }

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


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

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

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

    import { useState, useEffect } from 'react';
    import { fetchBio } from './api.js';
    
    export default function Page() {
      const [person, setPerson] = useState('Alice');
      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. Це гарантує, що ваш код не постраждає від "стану перегонів":відповіді від мережі можуть надходити у іншому порядку, ніж ви їх надсилали.

    import { useState, useEffect } from 'react';
    import { fetchBio } from './api.js';
    
    export default function Page() {
      const [person, setPerson] = useState('Alice');
      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="Alice">Alice</option>
            <option value="Bob">Bob</option>
            <option value="Taylor">Taylor</option>
          </select>
          <hr />
          <p><i>{bio ?? 'Loading...'}</i></p>
        </>
      );
    }
    export async function fetchBio(person) {
      const delay = person === 'Bob' ? 2000 : 200;
      return new Promise(resolve => {
        setTimeout(() => {
          resolve('This is ' + person + '’s bio.');
        }, delay);
      })
    }

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

    import { useState, useEffect } from 'react';
    import { fetchBio } from './api.js';
    
    export default function Page() {
      const [person, setPerson] = useState('Alice');
      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="Alice">Alice</option>
            <option value="Bob">Bob</option>
            <option value="Taylor">Taylor</option>
          </select>
          <hr />
          <p><i>{bio ?? 'Loading...'}</i></p>
        </>
      );
    }
    export async function fetchBio(person) {
      const delay = person === 'Bob' ? 2000 : 200;
      return new Promise(resolve => {
        setTimeout(() => {
          resolve('This is ' + person + '’s bio.');
        }, delay);
      })
    }

    Записування отримання даних безпосередньо в Effects призводить до повторень і ускладнює подальше додавання оптимізацій, таких як кешування та серверний рендеринг. Простіше скористатися власним хуком - вашим власним або підтримуваним спільнотою.

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

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

    • Ефекти не виконуються на сервері. Це означає, що початковий серверний HTML буде містити лише стан завантаження без даних. Клієнтському комп'ютеру доведеться завантажити весь JavaScript і відрендерити ваш застосунок лише для того, щоб виявити, що тепер йому потрібно завантажити дані. Це не дуже ефективно.
    • Отримання даних безпосередньо в ефектах полегшує створення "мережевих водоспадів". Ви рендерите батьківський компонент, він отримує деякі дані, рендерить дочірні компоненти, а потім вони починають отримувати свої дані. Якщо мережа не дуже швидка, це значно повільніше, ніж отримувати всі дані паралельно.
    • Отримання даних безпосередньо в ефектах зазвичай означає, що ви не завантажуєте і не кешуєте дані. Наприклад, якщо компонент демонтується, а потім монтується знову, йому доведеться отримати дані знову.
    • Це не дуже ергономічно. При написанні викликів fetch задіяно досить багато шаблонного коду, який не страждає від помилок на кшталт станів гонитви.

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

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

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


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

    Зверніть увагу, що ви не можете "вибирати" залежності вашого ефекту. Кожне значення реактивності</CodeStep> , що використовується кодом вашого ефекту, має бути оголошено як залежність. Список залежностей вашого ефекту визначається навколишнім кодом:</p> <pre><code data-meta="[[2, 1, "roomId"], [2, 2, "serverUrl"], [2, 5, "serverUrl"], [2, 5, "roomId" ], [2, 8, "serverUrl"], [2, 8, "roomId"]]" class="language-js">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, ваш ефект буде перепідключено до чату з використанням нових значень.

    Реактивні значення включають пропси та всі змінні і функції, оголошені безпосередньо всередині вашого компонента. Оскільки 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 has missing dependencies: 'roomId' and 'serverUrl'
      // ...
    }

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

    const serverUrl = 'https://localhost:1234'; // Not a reactive value anymore
    
    function ChatRoom({ roomId }) {
      useEffect(() => {
        const connection = createConnection(serverUrl, roomId);
        connection.connect();
        return () => connection.disconnect();
      }, [roomId]); // ✅ All dependencies declared
      // ...
    }

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

    const serverUrl = 'https://localhost:1234'; // Not a reactive value anymore
    const roomId = 'music'; // Not a reactive value anymore
    
    function ChatRoom() {
      useEffect(() => {
        const connection = createConnection(serverUrl, roomId);
        connection.connect();
        return () => connection.disconnect();
      }, []); // ✅ All dependencies declared
      // ...
    }

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

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

    useEffect(() => {
      // ...
      // 🔴 Avoid suppressing the linter like this:
      // eslint-ignore-next-line react-hooks/exhaustive-deps
    }, []);

    Коли залежності не збігаються з кодом, існує високий ризик внесення помилок. Пригнічуючи лінтер, ви "брешете" React про значення, від яких залежить ваш ефект. Натомість доведіть, що вони не потрібні.

    Передача масиву залежностей

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

    useEffect(() => {
      // ...
    }, [a, b]); // Runs again if a or b are different

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

    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>
            Server URL:{' '}
            <input
              value={serverUrl}
              onChange={e => setServerUrl(e.target.value)}
            />
          </label>
          <h1>Welcome to the {roomId} room!</h1>
          <label>
            Your message:{' '}
            <input value={message} onChange={e => setMessage(e.target.value)} />
          </label>
        </>
      );
    }
    
    export default function App() {
      const [show, setShow] = useState(false);
      const [roomId, setRoomId] = useState('general');
      return (
        <>
          <label>
            Choose the chat room:{' '}
            <select
              value={roomId}
              onChange={e => setRoomId(e.target.value)}
            >
              <option value="general">general</option>
              <option value="travel">travel</option>
              <option value="music">music</option>
            </select>
            <button onClick={() => setShow(!show)}>
              {show ? 'Close chat' : 'Open chat'}
            </button>
          </label>
          {show && <hr />}
          {show && <ChatRoom roomId={roomId}/>}
        </>
      );
    }
    export function createConnection(serverUrl, roomId) {
      // A real implementation would actually connect to the server
      return {
        connect() {
          console.log('✅ Connecting to "' + roomId + '" room at ' + serverUrl + '...');
        },
        disconnect() {
          console.log('❌ Disconnected from "' + roomId + '" room at ' + serverUrl);
        }
      };
    }
    input { margin-bottom: 10px; }
    button { margin-left: 5px; }

    Передача порожнього масиву залежностей

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

    useEffect(() => {
      // ...
    }, []); // Does not run again (except once in development)

    Навіть з порожніми залежностями встановлення та очищення буде запущено додатковий раз під час розробки, щоб допомогти вам знайти вади.

    У цьому прикладі і serverUrl, і roomId є жорстко закодованими. Оскільки вони оголошені за межами компонента, вони не є реактивними значеннями, а отже, не є залежностями. Список залежностей порожній, тому ефект не запускається повторно при повторному рендерингу.

    import { useState, useEffect } from 'react';
    import { createConnection } from './chat.js';
    
    const serverUrl = 'https://localhost:1234';
    const roomId = 'music';
    
    function ChatRoom() {
      const [message, setMessage] = useState('');
    
      useEffect(() => {
        const connection = createConnection(serverUrl, roomId);
        connection.connect();
        return () => connection.disconnect();
      }, []);
    
      return (
        <>
          <h1>Welcome to the {roomId} room!</h1>
          <label>
            Your message:{' '}
            <input value={message} onChange={e => setMessage(e.target.value)} />
          </label>
        </>
      );
    }
    
    export default function App() {
      const [show, setShow] = useState(false);
      return (
        <>
          <button onClick={() => setShow(!show)}>
            {show ? 'Close chat' : 'Open chat'}
          </button>
          {show && <hr />}
          {show && <ChatRoom />}
        </>
      );
    }
    export function createConnection(serverUrl, roomId) {
      // A real implementation would actually connect to the server
      return {
        connect() {
          console.log('✅ Connecting to "' + roomId + '" room at ' + serverUrl + '...');
        },
        disconnect() {
          console.log('❌ Disconnected from "' + roomId + '" room at ' + serverUrl);
        }
      };
    }

    Передача без масиву залежностей

    Якщо ви не передасте жодного масиву залежностей, ваш ефект запускатиметься після кожного рендерингу (і повторного рендерингу) вашого компонента.

    useEffect(() => {
      // ...
    }); // Always runs again

    У цьому прикладі ефект повторно запускається, коли ви змінюєте serverUrl і roomId, що є розумним. Однак він також повторно запускається, коли ви змінюєте повідомлення , що, ймовірно, небажано. Тому зазвичай ви вказуєте масив залежностей.

    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();
        };
      }); // No dependency array at all
    
      return (
        <>
          <label>
            Server URL:{' '}
            <input
              value={serverUrl}
              onChange={e => setServerUrl(e.target.value)}
            />
          </label>
          <h1>Welcome to the {roomId} room!</h1>
          <label>
            Your message:{' '}
            <input value={message} onChange={e => setMessage(e.target.value)} />
          </label>
        </>
      );
    }
    
    export default function App() {
      const [show, setShow] = useState(false);
      const [roomId, setRoomId] = useState('general');
      return (
        <>
          <label>
            Choose the chat room:{' '}
            <select
              value={roomId}
              onChange={e => setRoomId(e.target.value)}
            >
              <option value="general">general</option>
              <option value="travel">travel</option>
              <option value="music">music</option>
            </select>
            <button onClick={() => setShow(!show)}>
              {show ? 'Close chat' : 'Open chat'}
            </button>
          </label>
          {show && <hr />}
          {show && <ChatRoom roomId={roomId}/>}
        </>
      );
    }
    export function createConnection(serverUrl, roomId) {
      // A real implementation would actually connect to the server
      return {
        connect() {
          console.log('✅ Connecting to "' + roomId + '" room at ' + serverUrl + '...');
        },
        disconnect() {
          console.log('❌ Disconnected from "' + roomId + '" room at ' + serverUrl);
        }
      };
    }
    input { margin-bottom: 10px; }
    button { margin-left: 5px; }

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

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

    function Counter() {
      const [count, setCount] = useState(0);
    
      useEffect(() => {
        const intervalId = setInterval(() => {
          setCount(count + 1); // You want to increment the counter every second...
        }, 1000)
        return () => clearInterval(intervalId);
      }, [count]); // 🚩 ... but specifying `count` as a dependency always resets the interval.
      // ...
    }

    Оскільки 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); // ✅ Pass a state updater
        }, 1000);
        return () => clearInterval(intervalId);
      }, []); // ✅ Now count is not a dependency
    
      return <h1>{count}</h1>;
    }
    label {
      display: block;
      margin-top: 20px;
      margin-bottom: 20px;
    }
    
    body {
      min-height: 150px;
    }

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


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

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

    const serverUrl = 'https://localhost:1234';
    
    function ChatRoom({ roomId }) {
      const [message, setMessage] = useState('');
    
      const options = { // 🚩 This object is created from scratch on every re-render
        serverUrl: serverUrl,
        roomId: roomId
      };
    
      useEffect(() => {
        const connection = createConnection(options); // It's used inside the Effect
        connection.connect();
        return () => connection.disconnect();
      }, [options]); // 🚩 As a result, these dependencies are always different on a re-render
      // ...

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

    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>Welcome to the {roomId} room!</h1>
          <input value={message} onChange={e => setMessage(e.target.value)} />
        </>
      );
    }
    
    export default function App() {
      const [roomId, setRoomId] = useState('general');
      return (
        <>
          <label>
            Choose the chat room:{' '}
            <select
              value={roomId}
              onChange={e => setRoomId(e.target.value)}
            >
              <option value="general">general</option>
              <option value="travel">travel</option>
              <option value="music">music</option>
            </select>
          </label>
          <hr />
          <ChatRoom roomId={roomId} />
        </>
      );
    }
    export function createConnection({ serverUrl, roomId }) {
      // A real implementation would actually connect to the server
      return {
        connect() {
          console.log('✅ Connecting to "' + roomId + '" room at ' + serverUrl + '...');
        },
        disconnect() {
          console.log('❌ Disconnected from "' + roomId + '" room at ' + serverUrl);
        }
      };
    }
    input { display: block; margin-bottom: 20px; }
    button { margin-left: 10px; }

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

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


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

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

    function ChatRoom({ roomId }) {
      const [message, setMessage] = useState('');
    
      function createOptions() { // 🚩 This function is created from scratch on every re-render
        return {
          serverUrl: serverUrl,
          roomId: roomId
        };
      }
    
      useEffect(() => {
        const options = createOptions(); // It's used inside the Effect
        const connection = createConnection();
        connection.connect();
        return () => connection.disconnect();
      }, [createOptions]); // 🚩 As a result, these dependencies are always different on a re-render
      // ...

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

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

    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>Welcome to the {roomId} room!</h1>
          <input value={message} onChange={e => setMessage(e.target.value)} />
        </>
      );
    }
    
    export default function App() {
      const [roomId, setRoomId] = useState('general');
      return (
        <>
          <label>
            Choose the chat room:{' '}
            <select
              value={roomId}
              onChange={e => setRoomId(e.target.value)}
            >
              <option value="general">general</option>
              <option value="travel">travel</option>
              <option value="music">music</option>
            </select>
          </label>
          <hr />
          <ChatRoom roomId={roomId} />
        </>
      );
    }
    export function createConnection({ serverUrl, roomId }) {
      // A real implementation would actually connect to the server
      return {
        connect() {
          console.log('✅ Connecting to "' + roomId + '" room at ' + serverUrl + '...');
        },
        disconnect() {
          console.log('❌ Disconnected from "' + roomId + '" room at ' + serverUrl);
        }
      };
    }
    input { display: block; margin-bottom: 20px; }
    button { margin-left: 10px; }

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


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

    Цей розділ описує експериментальний API, який ще не було випущено у стабільній версії React.

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

    Втім, іноді вам потрібно прочитати останні пропси та стани з ефекту, не "реагуючи" на них. Наприклад, уявіть, що ви хочете записувати кількість товарів у кошику для кожного відвідування сторінки:

    function Page({ url, shoppingCart }) {
      useEffect(() => {
        logVisit(url, shoppingCart.length);
      }, [url, shoppingCart]); // ✅ All dependencies declared
      // ...
    }

    Що робити, якщо ви хочете реєструвати нове відвідування сторінки після кожної зміни URL, але не якщо змінюється лише shoppingCart? Ви не можете виключити shoppingCart із залежностей, не порушуючи правил реактивності. Однак, ви можете вказати, що ви не хочете, щоб частина коду "реагувала" на зміни, навіть якщо вона викликається з ефекту. Оголосіть подію ефекту за допомогою хука useEffectEvent і перемістіть код, що читає shoppingCart всередині неї:

    function Page({ url, shoppingCart }) {
      const onVisit = useEffectEvent(visitedUrl => {
        logVisit(visitedUrl, shoppingCart.length)
      });
    
      useEffect(() => {
        onVisit(url);
      }, [url]); // ✅ All dependencies declared
      // ...
    }

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

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


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

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

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

    function MyComponent() {
      const [didMount, setDidMount] = useState(false);
    
      useEffect(() => {
        setDidMount(true);
      }, []);
    
      if (didMount) {
        // ... return client-only JSX ...
      }  else {
        // ... return initial JSX ...
      }
    }

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

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


    Налагодження

    My Effect запускається двічі, коли компонент монтується

    .

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

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

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


    My Effect запускається після кожного повторного рендеру

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

    useEffect(() => {
      // ...
    }); // 🚩 No dependency array: re-runs after every render!

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

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

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

    Після цього ви можете клацнути правою кнопкою миші на масивах з різних рендерингів у консолі і вибрати "Зберегти як глобальну змінну" для обох. Якщо припустити, що перший масив було збережено як temp1, а другий - як temp2, ви можете скористатися консоллю браузера, щоб перевірити, чи кожна залежність в обох масивах однакова:

    Object.is(temp1[0], temp2[0]); // Is the first dependency the same between the arrays?
    Object.is(temp1[1], temp2[1]); // Is the second dependency the same between the arrays?
    Object.is(temp1[2], temp2[2]); // ... and so on for every dependency ...

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

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


    My Effect повторюється у нескінченному циклі

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

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

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

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

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

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


    Моя логіка очищення працює, хоча компонент не було демонтовано

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

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

    useEffect(() => {
      // 🔴 Avoid: Cleanup logic without corresponding setup logic
      return () => {
        doSomething();
      };
    }, []);

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

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

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


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

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