Люки ескейпу

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

Посилання на значення за допомогою рефів

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

const ref = useRef(0);

Як і стан, рефи зберігаються React між рендерингами. Однак, встановлення стану призводить до повторного рендерингу компонента. Зміна рефа цього не робить! Ви можете отримати доступ до поточного значення цього рефа через властивість ref.current.

import { useRef } from 'react';

export default function Counter() {
  let ref = useRef(0);

  function handleClick() {
    ref.current = ref.current + 1;
    alert('You clicked ' + ref.current + ' times!');
  }

  return (
    <button onClick={handleClick}>
      Click me!
    </button>
  );
}

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

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

Маніпуляції з DOM за допомогою рефів

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

import { useRef } from 'react';

export default function Form() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    <>
      <input ref={inputRef} />
      <button onClick={handleClick}>
        Focus the input
      </button>
    </>
  );
}

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

Синхронізація з ефектами

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

Натисніть кнопку відтворення/паузи кілька разів і подивіться, як відеоплеєр залишається синхронізованим зі значенням пропса isPlaying:

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

function VideoPlayer({ src, isPlaying }) {
  const ref = useRef(null);

  useEffect(() => {
    if (isPlaying) {
      ref.current.play();
    } else {
      ref.current.pause();
    }
  }, [isPlaying]);

  return <video ref={ref} src={src} loop playsInline />;
}

export default function App() {
  const [isPlaying, setIsPlaying] = useState(false);
  return (
    <>
      <button onClick={() => setIsPlaying(!isPlaying)}>
        {isPlaying ? 'Pause' : 'Play'}
      </button>
      <VideoPlayer
        isPlaying={isPlaying}
        src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
      />
    </>
  );
}
button { display: block; margin-bottom: 20px; }
video { width: 250px; }

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

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

export default function ChatRoom() {
  useEffect(() => {
    const connection = createConnection();
    connection.connect();
    return () => connection.disconnect();
  }, []);
  return <h1>Welcome to the chat!</h1>;
}
export function createConnection() {
  // A real implementation would actually connect to the server
  return {
    connect() {
      console.log('✅ Connecting...');
    },
    disconnect() {
      console.log('❌ Disconnected.');
    }
  };
}
input { display: block; margin-bottom: 20px; }

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

Прочитайте Синхронізація з ефектами, щоб дізнатися, як синхронізувати компоненти із зовнішніми системами.

Вам може не знадобитися ефект

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

Є два поширені випадки, коли ефекти не потрібні:

  • Вам не потрібні ефекти для перетворення даних для рендерингу.
  • Вам не потрібні ефекти для обробки подій користувача.

Наприклад, вам не потрібен ефект, щоб змінити один стан на основі іншого:

function Form() {
  const [firstName, setFirstName] = useState('Taylor');
  const [lastName, setLastName] = useState('Swift');

  // 🔴 Avoid: redundant state and unnecessary Effect
  const [fullName, setFullName] = useState('');
  useEffect(() => {
    setFullName(firstName + ' ' + lastName);
  }, [firstName, lastName]);
  // ...
}

Натомість обчислюйте якомога більше під час рендерингу:

function Form() {
  const [firstName, setFirstName] = useState('Taylor');
  const [lastName, setLastName] = useState('Swift');
  // ✅ Good: calculated during rendering
  const fullName = firstName + ' ' + lastName;
  // ...
}

Проте вам потрібні ефекти для синхронізації із зовнішніми системами.

Прочитайте Вам може не знадобитися ефект, щоб дізнатися, як видаляти непотрібні ефекти.

Життєвий цикл реактивних ефектів

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

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

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

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

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

Відокремлення подій від ефектів

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

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

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

{
  "dependencies": {
    "react": "latest",
    "react-dom": "latest",
    "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, useEffect } from 'react';
import { createConnection, sendMessage } from './chat.js';
import { showNotification } from './notifications.js';

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

function ChatRoom({ roomId, theme }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.on('connected', () => {
      showNotification('Connected!', theme);
    });
    connection.connect();
    return () => connection.disconnect();
  }, [roomId, theme]);

  return <h1>Welcome to the {roomId} room!</h1>
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  const [isDark, setIsDark] = 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>
      <label>
        <input
          type="checkbox"
          checked={isDark}
          onChange={e => setIsDark(e.target.checked)}
        />
        Use dark theme
      </label>
      <hr />
      <ChatRoom
        roomId={roomId}
        theme={isDark ? 'dark' : 'light'} 
      />
    </>
  );
}
export function createConnection(serverUrl, roomId) {
  // A real implementation would actually connect to the server
  let connectedCallback;
  let timeout;
  return {
    connect() {
      timeout = setTimeout(() => {
        if (connectedCallback) {
          connectedCallback();
        }
      }, 100);
    },
    on(event, callback) {
      if (connectedCallback) {
        throw Error('Cannot add the handler twice.');
      }
      if (event !== 'connected') {
        throw Error('Only "connected" event is supported.');
      }
      connectedCallback = callback;
    },
    disconnect() {
      clearTimeout(timeout);
    }
  };
}
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 { display: block; margin-top: 10px; }

Це не ідеально. Ви хочете повторно підключитися до чату, тільки якщо roomId змінився. Зміна теми не повинна призводити до перепідключення до чату! Перенесіть код, що читає theme з ефекту в Effect Event:

{
  "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, useEffect } from 'react';
import { experimental_useEffectEvent as useEffectEvent } from 'react';
import { createConnection, sendMessage } from './chat.js';
import { showNotification } from './notifications.js';

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

function ChatRoom({ roomId, theme }) {
  const onConnected = useEffectEvent(() => {
    showNotification('Connected!', theme);
  });

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

  return <h1>Welcome to the {roomId} room!</h1>
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  const [isDark, setIsDark] = 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>
      <label>
        <input
          type="checkbox"
          checked={isDark}
          onChange={e => setIsDark(e.target.checked)}
        />
        Use dark theme
      </label>
      <hr />
      <ChatRoom
        roomId={roomId}
        theme={isDark ? 'dark' : 'light'} 
      />
    </>
  );
}
export function createConnection(serverUrl, roomId) {
  // A real implementation would actually connect to the server
  let connectedCallback;
  let timeout;
  return {
    connect() {
      timeout = setTimeout(() => {
        if (connectedCallback) {
          connectedCallback();
        }
      }, 100);
    },
    on(event, callback) {
      if (connectedCallback) {
        throw Error('Cannot add the handler twice.');
      }
      if (event !== 'connected') {
        throw Error('Only "connected" event is supported.');
      }
      connectedCallback = callback;
    },
    disconnect() {
      clearTimeout(timeout);
    }
  };
}
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 { display: block; margin-top: 10px; }

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

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

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

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

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

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

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

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

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

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

  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:

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. Це було б неправильно. Натомість, ви змінили навколишній код так, що залежність стала непотрібною. Подумайте про список залежностей як про список усіх реактивних значень, що використовуються кодом вашого ефекту. Ви не навмисно вибираєте, що саме вносити до цього списку. Список описує ваш код. Щоб змінити список залежностей, змініть код.

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

Повторне використання логіки з користувацькими хуками

React постачається з вбудованими хуками, такими як useState, useContext та useEffect. Іноді вам може знадобитися хук для більш специфічних цілей: наприклад, для отримання даних, відстеження того, чи є користувач в мережі, або для підключення до чату. Для цього ви можете створити власні хуки для потреб вашої програми.

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

import { usePointerPosition } from './usePointerPosition.js';
import { useDelayedValue } from './useDelayedValue.js';

export default function Canvas() {
  const pos1 = usePointerPosition();
  const pos2 = useDelayedValue(pos1, 100);
  const pos3 = useDelayedValue(pos2, 200);
  const pos4 = useDelayedValue(pos3, 100);
  const pos5 = useDelayedValue(pos4, 50);
  return (
    <>
      <Dot position={pos1} opacity={1} />
      <Dot position={pos2} opacity={0.8} />
      <Dot position={pos3} opacity={0.6} />
      <Dot position={pos4} opacity={0.4} />
      <Dot position={pos5} opacity={0.2} />
    </>
  );
}

function Dot({ position, opacity }) {
  return (
    <div style={{
      position: 'absolute',
      backgroundColor: 'pink',
      borderRadius: '50%',
      opacity,
      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 usePointerPosition() {
  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 position;
}
import { useState, useEffect } from 'react';

export function useDelayedValue(value, delay) {
  const [delayedValue, setDelayedValue] = useState(value);

  useEffect(() => {
    setTimeout(() => {
      setDelayedValue(value);
    }, delay);
  }, [value, delay]);

  return delayedValue;
}
body { min-height: 300px; }

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

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

Що далі?

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