Видалення залежностей ефектів

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

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

Залежності повинні відповідати коду

Коли ви пишете ефект, ви спершу вказуєте, як запускати і зупиняти все, що ви хочете, щоб ваш ефект робив:

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

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

Тоді, якщо ви залишите залежності ефекту порожніми ([]), лінтер запропонує правильні залежності:

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

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

function ChatRoom({ roomId }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, []); // <-- Fix the mistake here!
  return <h1>Welcome to the {roomId} room!</h1>;
}

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; }

Заповніть їх відповідно до того, що каже лінтер:

function ChatRoom({ roomId }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]); // ✅ All dependencies declared
  // ...
}

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

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

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

function ChatRoom({ roomId }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);
  return <h1>Welcome to the {roomId} room!</h1>;
}

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; }

Щоб видалити залежність, доведіть, що вона не є залежністю

Зверніть увагу, що ви не можете "вибирати" залежності вашого ефекту. Кожне реактивне значення</Крок коду> , що використовуються кодом вашого ефекту, повинні бути оголошені у вашому списку залежностей. Список залежностей вашого ефекту визначається навколишнім кодом:</p> <pre><code data-meta="[[2, 3, "roomId"], [2, 5, "roomId"], [2, 8, "roomId"]]" class="language-js">const serverUrl = 'https://localhost:1234'; function ChatRoom({ roomId }) { // Це реактивне значення useEffect(() => { const connection = createConnection(serverUrl, roomId); // Цей ефект читає це реактивне значення connection.connect(); return () => connection.disconnect(); }, [roomId]); // ✅ Тому ви повинні вказати це реактивне значення як залежність вашого Ефекту // ... }

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

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

function ChatRoom({ roomId }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, []); // 🔴 React Hook useEffect has a missing dependency: 'roomId'
  // ...
}

І лінтер був би правильним! Оскільки roomId може змінюватися з часом, це призведе до помилки у вашому коді.

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

const serverUrl = 'https://localhost:1234';
const roomId = 'music'; // Not a reactive value anymore

function ChatRoom() {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, []); // ✅ All dependencies declared
  // ...
}

Тепер, оскільки roomId не є реактивним значенням (і не може змінюватися під час повторного рендерингу), йому не потрібно бути залежністю:

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

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

export default function ChatRoom() {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, []);
  return <h1>Welcome to the {roomId} room!</h1>;
}
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; }

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

Щоб змінити залежності, змініть код

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

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

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

Це може нагадувати розв'язування рівняння. Ви можете почати з мети (наприклад, видалити залежність), і вам потрібно "знайти" код, який відповідає цій меті. Не всім подобається розв'язувати рівняння, і те ж саме можна сказати про написання ефектів! На щастя, є список поширених рецептів, які ви можете спробувати нижче.

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

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

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

Натомість використовуйте наведені нижче методи.

Чому приховування лінтеру залежностей є таким небезпечним?

Вимкнення лінтера призводить до дуже неінтуїтивних вад, які важко знайти і виправити. Ось один з прикладів:

import { useState, useEffect } from 'react';

export default function Timer() {
  const [count, setCount] = useState(0);
  const [increment, setIncrement] = useState(1);

  function onTick() {
	setCount(count + increment);
  }

  useEffect(() => {
    const id = setInterval(onTick, 1000);
    return () => clearInterval(id);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <>
      <h1>
        Counter: {count}
        <button onClick={() => setCount(0)}>Reset</button>
      </h1>
      <hr />
      <p>
        Every second, increment by:
        <button disabled={increment === 0} onClick={() => {
          setIncrement(i => i - 1);
        }}>–</button>
        <b>{increment}</b>
        <button onClick={() => {
          setIncrement(i => i + 1);
        }}>+</button>
      </p>
    </>
  );
}
button { margin: 10px; }

Уявімо, що ви хочете застосувати ефект "лише на монтуванні". Ви прочитали, що порожні ([]) залежності роблять це, тому ви вирішили проігнорувати лінтер і примусово вказали [] як залежності.

Цей лічильник мав би щосекунди збільшуватися на величину, яку можна налаштувати за допомогою двох кнопок. Однак, оскільки ви "збрехали" React, що цей ефект ні від чого не залежить, React назавжди продовжує використовувати функцію onTick з початкового рендерингу. Під час того рендерингу count було 0, а інкремент був 1. Ось чому onTick з цього рендерингу щосекунди викликає setCount(0 + 1), і ви завжди бачите 1. Такі помилки важче виправити, коли вони поширюються на декілька компонентів.

Завжди є краще рішення, ніж ігнорувати літеру! Щоб виправити цей код, потрібно додати onTick до списку залежностей. (Щоб переконатися, що інтервал встановлюється лише один раз, зробіть onTick подією ефекту.)

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

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

Щоразу, коли ви налаштовуєте залежності ефекту для відображення коду, переглядайте список залежностей. Чи має сенс повторно запускати ефект, коли будь-яка з цих залежностей змінюється? Іноді відповідь "ні":

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

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

Цей код слід перенести в обробник подій?

Перше, про що вам слід подумати, - чи цей код взагалі має бути ефектом.

Уявіть собі форму. При відправленні ви встановлюєте змінну стану submitted у значення true. Вам потрібно відправити POST-запит і показати повідомлення. Ви помістили цю логіку всередину ефекту, який "реагує" на те, що submitted є true:

function Form() {
  const [submitted, setSubmitted] = useState(false);

  useEffect(() => {
    if (submitted) {
      // 🔴 Avoid: Event-specific logic inside an Effect
      post('/api/register');
      showNotification('Successfully registered!');
    }
  }, [submitted]);

  function handleSubmit() {
    setSubmitted(true);
  }

  // ...
}

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

function Form() {
  const [submitted, setSubmitted] = useState(false);
  const theme = useContext(ThemeContext);

  useEffect(() => {
    if (submitted) {
      // 🔴 Avoid: Event-specific logic inside an Effect
      post('/api/register');
      showNotification('Successfully registered!', theme);
    }
  }, [submitted, theme]); // ✅ All dependencies declared

  function handleSubmit() {
    setSubmitted(true);
  }  

  // ...
}

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

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

function Form() {
  const theme = useContext(ThemeContext);

  function handleSubmit() {
    // ✅ Good: Event-specific logic is called from event handlers
    post('/api/register');
    showNotification('Successfully registered!', theme);
  }  

  // ...
}

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

Ваш ефект виконує кілька непов'язаних між собою дій?

Наступне питання, яке вам слід задати собі - чи не виконує ваш ефект кілька непов'язаних між собою дій.

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

function ShippingForm({ country }) {
  const [cities, setCities] = useState(null);
  const [city, setCity] = useState(null);

  useEffect(() => {
    let ignore = false;
    fetch(`/api/cities?country=${country}`)
      .then(response => response.json())
      .then(json => {
        if (!ignore) {
          setCities(json);
        }
      });
    return () => {
      ignore = true;
    };
  }, [country]); // ✅ All dependencies declared

  // ...

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

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

function ShippingForm({ country }) {
  const [cities, setCities] = useState(null);
  const [city, setCity] = useState(null);
  const [areas, setAreas] = useState(null);

  useEffect(() => {
    let ignore = false;
    fetch(`/api/cities?country=${country}`)
      .then(response => response.json())
      .then(json => {
        if (!ignore) {
          setCities(json);
        }
      });
    // 🔴 Avoid: A single Effect synchronizes two independent processes
    if (city) {
      fetch(`/api/areas?city=${city}`)
        .then(response => response.json())
        .then(json => {
          if (!ignore) {
            setAreas(json);
          }
        });
    }
    return () => {
      ignore = true;
    };
  }, [country, city]); // ✅ All dependencies declared

  // ...

Втім, оскільки Effect тепер використовує змінну стану city, вам довелося додати city до списку залежностей. Це, у свою чергу, призвело до проблеми: коли користувач обирає інше місто, Effect повторно запускається і викликає fetchCities(country). Як наслідок, ви без потреби багато разів перезавантажуватимете список міст.

Проблема у цьому коді полягає у тому, що ви синхронізуєте дві різні непов'язані речі:

  1. Ви хочете синхронізувати стан cities з мережею на основі country prop.
  2. Ви хочете синхронізувати стан areas з мережею на основі стану city.

Розділити логіку на два ефекти, кожен з яких реагує на реквізит, з яким йому потрібно синхронізуватися:

function ShippingForm({ country }) {
  const [cities, setCities] = useState(null);
  useEffect(() => {
    let ignore = false;
    fetch(`/api/cities?country=${country}`)
      .then(response => response.json())
      .then(json => {
        if (!ignore) {
          setCities(json);
        }
      });
    return () => {
      ignore = true;
    };
  }, [country]); // ✅ All dependencies declared

  const [city, setCity] = useState(null);
  const [areas, setAreas] = useState(null);
  useEffect(() => {
    if (city) {
      let ignore = false;
      fetch(`/api/areas?city=${city}`)
        .then(response => response.json())
        .then(json => {
          if (!ignore) {
            setAreas(json);
          }
        });
      return () => {
        ignore = true;
      };
    }
  }, [city]); // ✅ All dependencies declared

  // ...

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

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

Ви читаєте деякий стан для обчислення наступного стану?

Цей ефект оновлює змінну стану messages новоствореним масивом щоразу, коли надходить нове повідомлення:

function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);
  useEffect(() => {
    const connection = createConnection();
    connection.connect();
    connection.on('message', (receivedMessage) => {
      setMessages([...messages, receivedMessage]);
    });
    // ...

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

function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);
  useEffect(() => {
    const connection = createConnection();
    connection.connect();
    connection.on('message', (receivedMessage) => {
      setMessages([...messages, receivedMessage]);
    });
    return () => connection.disconnect();
  }, [roomId, messages]); // ✅ All dependencies declared
  // ...

А створення повідомлень як залежності створює проблему.

Щоразу, коли ви отримуєте повідомлення, setMessages() компонент перезавантажується з новим масивом messages, який містить отримане повідомлення. Однак, оскільки цей ефект тепер залежить від messages, це призведе також до повторної синхронізації ефекту. Отже, кожне нове повідомлення змушуватиме чат перепідключатися. Користувачеві це не сподобається!

Щоб виправити проблему, не читайте повідомлення всередині ефекту. Натомість передайте функцію оновлення до setMessages:

function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);
  useEffect(() => {
    const connection = createConnection();
    connection.connect();
    connection.on('message', (receivedMessage) => {
      setMessages(msgs => [...msgs, receivedMessage]);
    });
    return () => connection.disconnect();
  }, [roomId]); // ✅ All dependencies declared
  // ...

Зверніть увагу, що ваш Effect тепер взагалі не читає змінну messages. Вам потрібно лише передати функцію оновлення на зразок msgs => [...msgs, receivedMessage]. React помістить вашу функцію оновлення в чергу і надасть їй аргумент msgs під час наступного рендеру. Таким чином, самому ефекту більше не потрібно залежати від messages. У результаті цього виправлення отримання повідомлення чату більше не призведе до перепідключення чату.

Хочете прочитати значення, не "реагуючи" на його зміни?

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

Припустимо, що ви хочете відтворювати звук, коли користувач отримує нове повідомлення, якщо isMuted не є істинним:

function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);
  const [isMuted, setIsMuted] = useState(false);

  useEffect(() => {
    const connection = createConnection();
    connection.connect();
    connection.on('message', (receivedMessage) => {
      setMessages(msgs => [...msgs, receivedMessage]);
      if (!isMuted) {
        playSound();
      }
    });
    // ...

Оскільки ваш ефект тепер використовує isMuted у своєму коді, вам слід додати його до залежностей:

function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);
  const [isMuted, setIsMuted] = useState(false);

  useEffect(() => {
    const connection = createConnection();
    connection.connect();
    connection.on('message', (receivedMessage) => {
      setMessages(msgs => [...msgs, receivedMessage]);
      if (!isMuted) {
        playSound();
      }
    });
    return () => connection.disconnect();
  }, [roomId, isMuted]); // ✅ All dependencies declared
  // ...

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

Щоб розв'язати цю проблему, вам потрібно вилучити з ефекту логіку, яка не повинна реагувати. Ви не хочете, щоб цей ефект "реагував" на зміни в isMuted. Перемістіть цей нереактивний фрагмент логіки у подію ефекту:

import { useState, useEffect, useEffectEvent } from 'react';

function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);
  const [isMuted, setIsMuted] = useState(false);

  const onMessage = useEffectEvent(receivedMessage => {
    setMessages(msgs => [...msgs, receivedMessage]);
    if (!isMuted) {
      playSound();
    }
  });

  useEffect(() => {
    const connection = createConnection();
    connection.connect();
    connection.on('message', (receivedMessage) => {
      onMessage(receivedMessage);
    });
    return () => connection.disconnect();
  }, [roomId]); // ✅ All dependencies declared
  // ...

За допомогою подій ефекту ви можете розділити ефект на реактивні частини (які мають "реагувати" на реактивні значення, як-от roomId та їхні зміни) та нереактивні частини (які лише зчитують їхні останні значення, як-от onMessage зчитує isMuted). Тепер, коли ви читаєте isMuted всередині події ефекту, вона не обов'язково має бути залежною від вашого ефекту.Внаслідок цього чат не буде повторно з'єднуватися, коли ви вмикаєте або вимикаєте параметр "Вимкнено", що вирішує початкову проблему!

Обгортання обробника події з пропсів

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

function ChatRoom({ roomId, onReceiveMessage }) {
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    const connection = createConnection();
    connection.connect();
    connection.on('message', (receivedMessage) => {
      onReceiveMessage(receivedMessage);
    });
    return () => connection.disconnect();
  }, [roomId, onReceiveMessage]); // ✅ All dependencies declared
  // ...

Припустимо, що батьківський компонент передає функцію onReceiveMessagedifferent onReceiveMessage при кожному рендерингу:

<ChatRoom
  roomId={roomId}
  onReceiveMessage={receivedMessage => {
    // ...
  }}
/>

Оскільки onReceiveMessage є залежністю, це призведе до повторної синхронізації ефекту після кожного батьківського рендерингу. Це призведе до повторного підключення до чату. Щоб вирішити цю проблему, обгорніть виклик у подію ефекту:

function ChatRoom({ roomId, onReceiveMessage }) {
  const [messages, setMessages] = useState([]);

  const onMessage = useEffectEvent(receivedMessage => {
    onReceiveMessage(receivedMessage);
  });

  useEffect(() => {
    const connection = createConnection();
    connection.connect();
    connection.on('message', (receivedMessage) => {
      onMessage(receivedMessage);
    });
    return () => connection.disconnect();
  }, [roomId]); // ✅ All dependencies declared
  // ...

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

Відокремлення реактивного та нереактивного коду

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

Вирішенням знову ж таки є виділення нереактивного коду у подію ефекту:

function Chat({ roomId, notificationCount }) {
  const onVisit = useEffectEvent(visitedRoomId => {
    logVisit(visitedRoomId, notificationCount);
  });

  useEffect(() => {
    onVisit(roomId);
  }, [roomId]); // ✅ All dependencies declared
  // ...
}

Ви хочете, щоб ваша логіка реагувала на roomId, тому ви читаєте roomId всередині вашого ефекту. Однак ви не хочете, щоб зміна notificationCount реєструвала додаткове відвідування, тому ви читаєте notificationCount всередині події ефекту. Дізнайтеся більше про читання найновіших пропсів та станів ефектів за допомогою подій ефектів.

Чи змінюється якесь реактивне значення ненавмисно?

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

function ChatRoom({ roomId }) {
  // ...
  const options = {
    serverUrl: serverUrl,
    roomId: roomId
  };

  useEffect(() => {
    const connection = createConnection(options);
    connection.connect();
    // ...

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

// ...
  useEffect(() => {
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, [options]); // ✅ All dependencies declared
  // ...

Важливо оголосити його як залежність! Це гарантує, наприклад, що якщо зміниться roomId, ваш Effect перепідключиться до чату з новими опціями. Однак, у наведеному вище коді також є проблема. Щоб побачити її, спробуйте ввести дані в пісочниці нижче і подивіться, що станеться в консолі:

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

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

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

  // Temporarily disable the linter to demonstrate the problem
  // eslint-disable-next-line react-hooks/exhaustive-deps
  const options = {
    serverUrl: serverUrl,
    roomId: roomId
  };

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

  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; }

У наведеній вище пісочниці вхідні дані лише оновлюють змінну стану message. З точки зору користувача, це не повинно вплинути на з'єднання з чатом. Однак, кожного разу, коли ви оновлюєте message, ваш компонент повторно рендериться. Коли ваш компонент перезавантажується, код всередині нього виконується знову з нуля.

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

Ця проблема стосується лише об'єктів та функцій. У JavaScript кожен новостворений об'єкт і функція вважається відмінним від усіх інших. Неважливо, що вміст всередині них може бути однаковим!

// During the first render
const options1 = { serverUrl: 'https://localhost:1234', roomId: 'music' };

// During the next render
const options2 = { serverUrl: 'https://localhost:1234', roomId: 'music' };

// These are two different objects!
console.log(Object.is(options1, options2)); // false

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

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

Перемістіть статичні об'єкти та функції за межі вашого компонента

Якщо об'єкт не залежить від жодних пропсів та станів, ви можете перемістити цей об'єкт за межі вашого компонента:

const options = {
  serverUrl: 'https://localhost:1234',
  roomId: 'music'
};

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

  useEffect(() => {
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, []); // ✅ All dependencies declared
  // ...

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

Це працює і для функцій:

function createOptions() {
  return {
    serverUrl: 'https://localhost:1234',
    roomId: 'music'
  };
}

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

  useEffect(() => {
    const options = createOptions();
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, []); // ✅ All dependencies declared
  // ...

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

Переміщення динамічних об'єктів та функцій всередині ефекту

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

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]); // ✅ All dependencies declared
  // ...

Тепер, коли опції оголошено всередині вашого ефекту, вони більше не є залежними від вашого ефекту. Натомість, єдиним реактивним значенням, що використовується вашим ефектом, є roomId. Оскільки roomId не є об'єктом або функцією, ви можете бути впевнені, що він не буде ненавмисно відрізнятися. У JavaScript числа та рядки порівнюються за їх вмістом:

// During the first render
const roomId1 = 'music';

// During the next render
const roomId2 = 'music';

// These two strings are the same!
console.log(Object.is(roomId1, roomId2)); // true

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

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; }

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

Це працює і для функцій:

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]); // ✅ All dependencies declared
  // ...

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

Читати примітивні значення з об'єктів

Іноді ви можете отримати об'єкт з пропсів:

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

  useEffect(() => {
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, [options]); // ✅ All dependencies declared
  // ...

Ризик полягає у тому, що батьківський компонент створить об'єкт під час рендерингу:

<ChatRoom
  roomId={roomId}
  options={{
    serverUrl: serverUrl,
    roomId: roomId
  }}
/>

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

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

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

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

Обчислення примітивних значень з функцій

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

<ChatRoom
  roomId={roomId}
  getOptions={() => {
    return {
      serverUrl: serverUrl,
      roomId: roomId
    };
  }}
/>

Щоб уникнути створення залежності (і повторного підключення при повторному рендерингу), викликайте його поза ефектом. Це дасть вам значення roomId та serverUrl, які не є об'єктами, і які ви можете прочитати зсередини ефекту:

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

  const { roomId, serverUrl } = getOptions();
  useEffect(() => {
    const connection = createConnection({
      roomId: roomId,
      serverUrl: serverUrl
    });
    connection.connect();
    return () => connection.disconnect();
  }, [roomId, serverUrl]); // ✅ All dependencies declared
  // ...

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

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

Виправити інтервал скидання

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

Здається, що код цього ефекту залежить від count. Чи можна якось позбутися цієї залежності? Має бути спосіб оновити стан count на основі його попереднього значення без додавання залежності від цього значення.

import { useState, useEffect } from 'react';

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

  useEffect(() => {
    console.log('✅ Creating an interval');
    const id = setInterval(() => {
      console.log('⏰ Interval tick');
      setCount(count + 1);
    }, 1000);
    return () => {
      console.log('❌ Clearing an interval');
      clearInterval(id);
    };
  }, [count]);

  return <h1>Counter: {count}</h1>
}

Ви хочете оновити стан count на count + 1 всередині ефекту. Однак це зробить ваш ефект залежним від count, який змінюється з кожною відміткою, і тому ваш інтервал буде створюватися наново з кожною відміткою.

Щоб вирішити цю проблему, скористайтеся функцією updater, а замість setCount(count + 1):

напишіть setCount(c => c + 1)
import { useState, useEffect } from 'react';

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

  useEffect(() => {
    console.log('✅ Creating an interval');
    const id = setInterval(() => {
      console.log('⏰ Interval tick');
      setCount(c => c + 1);
    }, 1000);
    return () => {
      console.log('❌ Clearing an interval');
      clearInterval(id);
    };
  }, []);

  return <h1>Counter: {count}</h1>
}

Замість того, щоб читати count всередині ефекту, ви передаєте інструкцію c => c + 1 ("збільшити це число!") до React. React застосує її при наступному рендері. І оскільки вам більше не потрібно читати значення count всередині вашого ефекту, ви можете залишити залежності ефекту порожніми ([]). Це запобігає повторному створенню інтервалу ефектом на кожному тику.

Виправити анімацію повторного спрацьовування

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

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

Чи є всередині ефекту рядок коду, який не повинен бути реактивним? Як перемістити нереактивний код з ефекту?

{
  "dependencies": {
    "react": "experimental",
    "react-dom": "experimental",
    "react-scripts": "latest"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  }
}
import { useState, useEffect, useRef } from 'react';
import { experimental_useEffectEvent as useEffectEvent } from 'react';
import { FadeInAnimation } from './animation.js';

function Welcome({ duration }) {
  const ref = useRef(null);

  useEffect(() => {
    const animation = new FadeInAnimation(ref.current);
    animation.start(duration);
    return () => {
      animation.stop();
    };
  }, [duration]);

  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 [duration, setDuration] = useState(1000);
  const [show, setShow] = useState(false);

  return (
    <>
      <label>
        <input
          type="range"
          min="100"
          max="3000"
          value={duration}
          onChange={e => setDuration(Number(e.target.value))}
        />
        <br />
        Fade in duration: {duration} ms
      </label>
      <button onClick={() => setShow(!show)}>
        {show ? 'Remove' : 'Show'}
      </button>
      <hr />
      {show && <Welcome duration={duration} />}
    </>
  );
}
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; }

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

{
  "dependencies": {
    "react": "experimental",
    "react-dom": "experimental",
    "react-scripts": "latest"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  }
}
import { useState, useEffect, useRef } from 'react';
import { FadeInAnimation } from './animation.js';
import { experimental_useEffectEvent as useEffectEvent } from 'react';

function Welcome({ duration }) {
  const ref = useRef(null);

  const onAppear = useEffectEvent(animation => {
    animation.start(duration);
  });

  useEffect(() => {
    const animation = new FadeInAnimation(ref.current);
    onAppear(animation);
    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 [duration, setDuration] = useState(1000);
  const [show, setShow] = useState(false);

  return (
    <>
      <label>
        <input
          type="range"
          min="100"
          max="3000"
          value={duration}
          onChange={e => setDuration(Number(e.target.value))}
        />
        <br />
        Fade in duration: {duration} ms
      </label>
      <button onClick={() => setShow(!show)}>
        {show ? 'Remove' : 'Show'}
      </button>
      <hr />
      {show && <Welcome duration={duration} />}
    </>
  );
}
export class FadeInAnimation {
  constructor(node) {
    this.node = node;
  }
  start(duration) {
    this.duration = duration;
    this.onProgress(0);
    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; }

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

Виправлено помилку перепідключення чату

У цьому прикладі щоразу, коли ви натискаєте "Переключити тему", чат перепідключається. Чому це відбувається? Виправте помилку так, щоб чат перепідключався лише тоді, коли ви редагуєте URL-адресу сервера або вибираєте іншу кімнату чату.

Ставтеся до chat.js як до зовнішньої сторонньої бібліотеки: ви можете звернутися до неї, щоб перевірити її API, але не редагуйте її.

Існує кілька способів виправити це, але, зрештою, ви хочете уникнути залежності від об'єкта.

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

export default function App() {
  const [isDark, setIsDark] = useState(false);
  const [roomId, setRoomId] = useState('general');
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

  const options = {
    serverUrl: serverUrl,
    roomId: roomId
  };

  return (
    <div className={isDark ? 'dark' : 'light'}>
      <button onClick={() => setIsDark(!isDark)}>
        Toggle theme
      </button>
      <label>
        Server URL:{' '}
        <input
          value={serverUrl}
          onChange={e => setServerUrl(e.target.value)}
        />
      </label>
      <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 options={options} />
    </div>
  );
}
import { useEffect } from 'react';
import { createConnection } from './chat.js';

export default function ChatRoom({ options }) {
  useEffect(() => {
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, [options]);

  return <h1>Welcome to the {options.roomId} room!</h1>;
}
export function createConnection({ serverUrl, roomId }) {
  // A real implementation would actually connect to the server
  if (typeof serverUrl !== 'string') {
    throw Error('Expected serverUrl to be a string. Received: ' + serverUrl);
  }
  if (typeof roomId !== 'string') {
    throw Error('Expected roomId to be a string. Received: ' + roomId);
  }
  return {
    connect() {
      console.log('✅ Connecting to "' + roomId + '" room at ' + serverUrl + '...');
    },
    disconnect() {
      console.log('❌ Disconnected from "' + roomId + '" room at ' + serverUrl);
    }
  };
}
label, button { display: block; margin-bottom: 5px; }
.dark { background: #222; color: #eee; }

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

Найменш інвазивне виправлення - прочитати roomId і serverUrl безпосередньо за межами ефекту, а потім зробити ефект залежним від цих примітивних значень (які не можуть змінюватися ненавмисно). Всередині ефекту створіть об'єкт і передайте його до createConnection:

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

export default function App() {
  const [isDark, setIsDark] = useState(false);
  const [roomId, setRoomId] = useState('general');
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

  const options = {
    serverUrl: serverUrl,
    roomId: roomId
  };

  return (
    <div className={isDark ? 'dark' : 'light'}>
      <button onClick={() => setIsDark(!isDark)}>
        Toggle theme
      </button>
      <label>
        Server URL:{' '}
        <input
          value={serverUrl}
          onChange={e => setServerUrl(e.target.value)}
        />
      </label>
      <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 options={options} />
    </div>
  );
}
import { useEffect } from 'react';
import { createConnection } from './chat.js';

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

  return <h1>Welcome to the {options.roomId} room!</h1>;
}
export function createConnection({ serverUrl, roomId }) {
  // A real implementation would actually connect to the server
  if (typeof serverUrl !== 'string') {
    throw Error('Expected serverUrl to be a string. Received: ' + serverUrl);
  }
  if (typeof roomId !== 'string') {
    throw Error('Expected roomId to be a string. Received: ' + roomId);
  }
  return {
    connect() {
      console.log('✅ Connecting to "' + roomId + '" room at ' + serverUrl + '...');
    },
    disconnect() {
      console.log('❌ Disconnected from "' + roomId + '" room at ' + serverUrl);
    }
  };
}
label, button { display: block; margin-bottom: 5px; }
.dark { background: #222; color: #eee; }

Ще краще було б замінити об'єкт пропу options на більш специфічні пропи roomId та serverUrl:

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

export default function App() {
  const [isDark, setIsDark] = useState(false);
  const [roomId, setRoomId] = useState('general');
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

  return (
    <div className={isDark ? 'dark' : 'light'}>
      <button onClick={() => setIsDark(!isDark)}>
        Toggle theme
      </button>
      <label>
        Server URL:{' '}
        <input
          value={serverUrl}
          onChange={e => setServerUrl(e.target.value)}
        />
      </label>
      <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}
        serverUrl={serverUrl}
      />
    </div>
  );
}
import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

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

  return <h1>Welcome to the {roomId} room!</h1>;
}
export function createConnection({ serverUrl, roomId }) {
  // A real implementation would actually connect to the server
  if (typeof serverUrl !== 'string') {
    throw Error('Expected serverUrl to be a string. Received: ' + serverUrl);
  }
  if (typeof roomId !== 'string') {
    throw Error('Expected roomId to be a string. Received: ' + roomId);
  }
  return {
    connect() {
      console.log('✅ Connecting to "' + roomId + '" room at ' + serverUrl + '...');
    },
    disconnect() {
      console.log('❌ Disconnected from "' + roomId + '" room at ' + serverUrl);
    }
  };
}
label, button { display: block; margin-bottom: 5px; }
.dark { background: #222; color: #eee; }

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

Виправлено помилку перепідключення чату, знову

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

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

Не змінюйте код у chat.js. Окрім цього, ви можете змінювати будь-який код, якщо це призведе до однакової поведінки. Наприклад, вам може бути корисно змінити пропси, які передаються.

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

Однією з таких функцій є обробник подій. Чи знаєте ви спосіб викликати обробник події як Ефект, не "реагуючи" на нові значення функції обробника події? Це було б дуже корисно!

Ще одна з цих функцій існує лише для того, щоб передати деякий стан імпортованому методу API. Чи дійсно ця функція необхідна? Яка важлива інформація передається? Можливо, вам доведеться перемістити деякі імпортовані дані з App.js до ChatRoom.js.

{
  "dependencies": {
    "react": "experimental",
    "react-dom": "experimental",
    "react-scripts": "latest",
    "toastify-js": "1.12.0"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  }
}
import { useState } from 'react';
import ChatRoom from './ChatRoom.js';
import {
  createEncryptedConnection,
  createUnencryptedConnection,
} from './chat.js';
import { showNotification } from './notifications.js';

export default function App() {
  const [isDark, setIsDark] = useState(false);
  const [roomId, setRoomId] = useState('general');
  const [isEncrypted, setIsEncrypted] = useState(false);

  return (
    <>
      <label>
        <input
          type="checkbox"
          checked={isDark}
          onChange={e => setIsDark(e.target.checked)}
        />
        Use dark theme
      </label>
      <label>
        <input
          type="checkbox"
          checked={isEncrypted}
          onChange={e => setIsEncrypted(e.target.checked)}
        />
        Enable encryption
      </label>
      <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}
        onMessage={msg => {
          showNotification('New message: ' + msg, isDark ? 'dark' : 'light');
        }}
        createConnection={() => {
          const options = {
            serverUrl: 'https://localhost:1234',
            roomId: roomId
          };
          if (isEncrypted) {
            return createEncryptedConnection(options);
          } else {
            return createUnencryptedConnection(options);
          }
        }}
      />
    </>
  );
}
import { useState, useEffect } from 'react';
import { experimental_useEffectEvent as useEffectEvent } from 'react';

export default function ChatRoom({ roomId, createConnection, onMessage }) {
  useEffect(() => {
    const connection = createConnection();
    connection.on('message', (msg) => onMessage(msg));
    connection.connect();
    return () => connection.disconnect();
  }, [createConnection, onMessage]);

  return <h1>Welcome to the {roomId} room!</h1>;
}
export function createEncryptedConnection({ serverUrl, roomId }) {
  // A real implementation would actually connect to the server
  if (typeof serverUrl !== 'string') {
    throw Error('Expected serverUrl to be a string. Received: ' + serverUrl);
  }
  if (typeof roomId !== 'string') {
    throw Error('Expected roomId to be a string. Received: ' + roomId);
  }
  let intervalId;
  let messageCallback;
  return {
    connect() {
      console.log('✅ 🔐 Connecting to "' + roomId + '" room... (encrypted)');
      clearInterval(intervalId);
      intervalId = setInterval(() => {
        if (messageCallback) {
          if (Math.random() > 0.5) {
            messageCallback('hey')
          } else {
            messageCallback('lol');
          }
        }
      }, 3000);
    },
    disconnect() {
      clearInterval(intervalId);
      messageCallback = null;
      console.log('❌ 🔐 Disconnected from "' + roomId + '" room (encrypted)');
    },
    on(event, callback) {
      if (messageCallback) {
        throw Error('Cannot add the handler twice.');
      }
      if (event !== 'message') {
        throw Error('Only "message" event is supported.');
      }
      messageCallback = callback;
    },
  };
}

export function createUnencryptedConnection({ serverUrl, roomId }) {
  // A real implementation would actually connect to the server
  if (typeof serverUrl !== 'string') {
    throw Error('Expected serverUrl to be a string. Received: ' + serverUrl);
  }
  if (typeof roomId !== 'string') {
    throw Error('Expected roomId to be a string. Received: ' + roomId);
  }
  let intervalId;
  let messageCallback;
  return {
    connect() {
      console.log('✅ Connecting to "' + roomId + '" room (unencrypted)...');
      clearInterval(intervalId);
      intervalId = setInterval(() => {
        if (messageCallback) {
          if (Math.random() > 0.5) {
            messageCallback('hey')
          } else {
            messageCallback('lol');
          }
        }
      }, 3000);
    },
    disconnect() {
      clearInterval(intervalId);
      messageCallback = null;
      console.log('❌ Disconnected from "' + roomId + '" room (unencrypted)');
    },
    on(event, callback) {
      if (messageCallback) {
        throw Error('Cannot add the handler twice.');
      }
      if (event !== 'message') {
        throw Error('Only "message" event is supported.');
      }
      messageCallback = callback;
    },
  };
}
import Toastify from 'toastify-js';
import 'toastify-js/src/toastify.css';

export function showNotification(message, theme) {
  Toastify({
    text: message,
    duration: 2000,
    gravity: 'top',
    position: 'right',
    style: {
      background: theme === 'dark' ? 'black' : 'white',
      color: theme === 'dark' ? 'white' : 'black',
    },
  }).showToast();
}
label, button { display: block; margin-bottom: 5px; }

Існує кілька правильних способів розв'язання, але ось один з можливих.

У початковому прикладі перемикання теми призводило до створення та передачі різних функцій onMessage та createConnection. Оскільки ефект залежав від цих функцій, чат перепідключався щоразу, коли ви змінювали тему.

Щоб виправити проблему з onMessage, потрібно було обгорнути його у подію ефекту:

export default function ChatRoom({ roomId, createConnection, onMessage }) {
  const onReceiveMessage = useEffectEvent(onMessage);

  useEffect(() => {
    const connection = createConnection();
    connection.on('message', (msg) => onReceiveMessage(msg));
    // ...

На відміну від пропсу onMessage, подія ефекту onReceiveMessage не є реактивною. Ось чому вона не повинна бути залежною від вашого ефекту. Як наслідок, зміни у onMessage не призведуть до перепідключення чату.

Ви не можете зробити те саме з createConnection, оскільки він має реагувати. Ви хочете, щоб ефект спрацьовував повторно, якщо користувач перемикається між зашифрованим і незашифрованим з'єднанням або якщо користувач змінює поточну кімнату. Однак, оскільки createConnection є функцією, ви не можете перевірити, чи дійсно зчитана нею інформація змінилася чи ні. Щоб вирішити цю проблему, замість того, щоб передавати createConnection з компонента App, передайте необроблені значення roomId і isEncrypted:

<ChatRoom
        roomId={roomId}
        isEncrypted={isEncrypted}
        onMessage={msg => {
          showNotification('New message: ' + msg, isDark ? 'dark' : 'light');
        }}
      />

Тепер ви можете перемістити функцію createConnection всередину ефекту замість того, щоб передавати її з App:

import {
  createEncryptedConnection,
  createUnencryptedConnection,
} from './chat.js';

export default function ChatRoom({ roomId, isEncrypted, onMessage }) {
  const onReceiveMessage = useEffectEvent(onMessage);

  useEffect(() => {
    function createConnection() {
      const options = {
        serverUrl: 'https://localhost:1234',
        roomId: roomId
      };
      if (isEncrypted) {
        return createEncryptedConnection(options);
      } else {
        return createUnencryptedConnection(options);
      }
    }
    // ...

Після цих двох змін ваш ефект більше не залежить від значень функцій:

export default function ChatRoom({ roomId, isEncrypted, onMessage }) { // Reactive values
  const onReceiveMessage = useEffectEvent(onMessage); // Not reactive

  useEffect(() => {
    function createConnection() {
      const options = {
        serverUrl: 'https://localhost:1234',
        roomId: roomId // Reading a reactive value
      };
      if (isEncrypted) { // Reading a reactive value
        return createEncryptedConnection(options);
      } else {
        return createUnencryptedConnection(options);
      }
    }

    const connection = createConnection();
    connection.on('message', (msg) => onReceiveMessage(msg));
    connection.connect();
    return () => connection.disconnect();
  }, [roomId, isEncrypted]); // ✅ All dependencies declared

Внаслідок цього чат перепідключається лише тоді, коли змінюється щось значуще (roomId або isEncrypted):

{
  "dependencies": {
    "react": "experimental",
    "react-dom": "experimental",
    "react-scripts": "latest",
    "toastify-js": "1.12.0"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  }
}
import { useState } from 'react';
import ChatRoom from './ChatRoom.js';

import { showNotification } from './notifications.js';

export default function App() {
  const [isDark, setIsDark] = useState(false);
  const [roomId, setRoomId] = useState('general');
  const [isEncrypted, setIsEncrypted] = useState(false);

  return (
    <>
      <label>
        <input
          type="checkbox"
          checked={isDark}
          onChange={e => setIsDark(e.target.checked)}
        />
        Use dark theme
      </label>
      <label>
        <input
          type="checkbox"
          checked={isEncrypted}
          onChange={e => setIsEncrypted(e.target.checked)}
        />
        Enable encryption
      </label>
      <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}
        isEncrypted={isEncrypted}
        onMessage={msg => {
          showNotification('New message: ' + msg, isDark ? 'dark' : 'light');
        }}
      />
    </>
  );
}
import { useState, useEffect } from 'react';
import { experimental_useEffectEvent as useEffectEvent } from 'react';
import {
  createEncryptedConnection,
  createUnencryptedConnection,
} from './chat.js';

export default function ChatRoom({ roomId, isEncrypted, onMessage }) {
  const onReceiveMessage = useEffectEvent(onMessage);

  useEffect(() => {
    function createConnection() {
      const options = {
        serverUrl: 'https://localhost:1234',
        roomId: roomId
      };
      if (isEncrypted) {
        return createEncryptedConnection(options);
      } else {
        return createUnencryptedConnection(options);
      }
    }

    const connection = createConnection();
    connection.on('message', (msg) => onReceiveMessage(msg));
    connection.connect();
    return () => connection.disconnect();
  }, [roomId, isEncrypted]);

  return <h1>Welcome to the {roomId} room!</h1>;
}
export function createEncryptedConnection({ serverUrl, roomId }) {
  // A real implementation would actually connect to the server
  if (typeof serverUrl !== 'string') {
    throw Error('Expected serverUrl to be a string. Received: ' + serverUrl);
  }
  if (typeof roomId !== 'string') {
    throw Error('Expected roomId to be a string. Received: ' + roomId);
  }
  let intervalId;
  let messageCallback;
  return {
    connect() {
      console.log('✅ 🔐 Connecting to "' + roomId + '" room... (encrypted)');
      clearInterval(intervalId);
      intervalId = setInterval(() => {
        if (messageCallback) {
          if (Math.random() > 0.5) {
            messageCallback('hey')
          } else {
            messageCallback('lol');
          }
        }
      }, 3000);
    },
    disconnect() {
      clearInterval(intervalId);
      messageCallback = null;
      console.log('❌ 🔐 Disconnected from "' + roomId + '" room (encrypted)');
    },
    on(event, callback) {
      if (messageCallback) {
        throw Error('Cannot add the handler twice.');
      }
      if (event !== 'message') {
        throw Error('Only "message" event is supported.');
      }
      messageCallback = callback;
    },
  };
}

export function createUnencryptedConnection({ serverUrl, roomId }) {
  // A real implementation would actually connect to the server
  if (typeof serverUrl !== 'string') {
    throw Error('Expected serverUrl to be a string. Received: ' + serverUrl);
  }
  if (typeof roomId !== 'string') {
    throw Error('Expected roomId to be a string. Received: ' + roomId);
  }
  let intervalId;
  let messageCallback;
  return {
    connect() {
      console.log('✅ Connecting to "' + roomId + '" room (unencrypted)...');
      clearInterval(intervalId);
      intervalId = setInterval(() => {
        if (messageCallback) {
          if (Math.random() > 0.5) {
            messageCallback('hey')
          } else {
            messageCallback('lol');
          }
        }
      }, 3000);
    },
    disconnect() {
      clearInterval(intervalId);
      messageCallback = null;
      console.log('❌ Disconnected from "' + roomId + '" room (unencrypted)');
    },
    on(event, callback) {
      if (messageCallback) {
        throw Error('Cannot add the handler twice.');
      }
      if (event !== 'message') {
        throw Error('Only "message" event is supported.');
      }
      messageCallback = callback;
    },
  };
}
import Toastify from 'toastify-js';
import 'toastify-js/src/toastify.css';

export function showNotification(message, theme) {
  Toastify({
    text: message,
    duration: 2000,
    gravity: 'top',
    position: 'right',
    style: {
      background: theme === 'dark' ? 'black' : 'white',
      color: theme === 'dark' ? 'white' : 'black',
    },
  }).showToast();
}
label, button { display: block; margin-bottom: 5px; }