Spec-Zone.ru › React

Использование логики с настраиваемыми хуками

React поставляется с несколькими встроенными хуками, такими как useState, useContext, и useEffect. Иногда вам захочется, чтобы существовал хук для более конкретной цели: например, для получения данных, отслеживания, подключен ли пользователь к сети, или для подключения к чату. Вы можете не найти эти хуки в React, но вы можете создать свои собственные хуки для потребностей вашего приложения.

Вы узнаете

  • Что такое настраиваемые хуки и как их написать
  • Как повторно использовать логику между компонентами
  • Как именовать и структурировать свои настраиваемые хуки
  • Когда и почему следует извлекать настраиваемые хуки

Настраиваемые хуки: Повторное использование логики между компонентами

Представьте, что вы разрабатываете приложение, которое сильно зависит от сети (как и большинство приложений). Вы хотите предупредить пользователя, если подключение к сети случайно оборвалось во время использования вашего приложения. Как вы это сделаете? Похоже, вам понадобятся две вещи в вашем компоненте:

  1. Часть состояния, которая отслеживает, подключена ли сеть.
  2. Эффект, который подписывается на глобальные события online и offline и обновляет это состояние.

Это позволит вашему компоненту оставаться синхронизированным со статусом сети. Вы можете начать с чего-то вроде этого:

import { useState, useEffect } from 'react';

export default function StatusBar() {
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    function handleOnline() {
      setIsOnline(true);
    }
    function handleOffline() {
      setIsOnline(false);
    }
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);

  return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}

Попробуйте включить и выключить сеть и обратите внимание, как это StatusBar обновляется в ответ на ваши действия.

Теперь представьте, что вы также хотите использовать ту же логику в другом компоненте. Вы хотите реализовать кнопку «Сохранить», которая будет отключаться и показывать «Подключение…», вместо «Сохранить», пока сеть отключена.

Для начала вы можете скопировать и вставить состояние isOnline и эффект в SaveButton:

import { useState, useEffect } from 'react';

export default function SaveButton() {
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    function handleOnline() {
      setIsOnline(true);
    }
    function handleOffline() {
      setIsOnline(false);
    }
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);

  function handleSaveClick() {
    console.log('✅ Progress saved');
  }

  return (
    <button disabled={!isOnline} onClick={handleSaveClick}>
      {isOnline ? 'Save progress' : 'Reconnecting...'}
    </button>
  );
}

Убедитесь, что при отключении сети кнопка изменит свой вид.

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

Извлечение собственного настраиваемого хука из компонента

Представьте на мгновение, что, аналогично useState и useEffect, существует встроенный useOnlineStatus хук. Тогда оба этих компонента можно упростить, и вы можете устранить дублирование между ними:

function StatusBar() {
  const isOnline = useOnlineStatus();
  return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}

function SaveButton() {
  const isOnline = useOnlineStatus();

  function handleSaveClick() {
    console.log('✅ Progress saved');
  }

  return (
    <button disabled={!isOnline} onClick={handleSaveClick}>
      {isOnline ? 'Save progress' : 'Reconnecting...'}
    </button>
  );
}

Хотя такого встроенного хука нет, вы можете написать его самостоятельно. Объявите функцию, названную useOnlineStatus, и перенесите весь дублирующийся код из ранее написанных компонентов в неё:

function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    function handleOnline() {
      setIsOnline(true);
    }
    function handleOffline() {
      setIsOnline(false);
    }
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);
  return isOnline;
}

В конце функции верните isOnline. Это позволит вашим компонентам считывать это значение:

import { useOnlineStatus } from './useOnlineStatus.js';

function StatusBar() {
  const isOnline = useOnlineStatus();
  return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}

function SaveButton() {
  const isOnline = useOnlineStatus();

  function handleSaveClick() {
    console.log('✅ Progress saved');
  }

  return (
    <button disabled={!isOnline} onClick={handleSaveClick}>
      {isOnline ? 'Save progress' : 'Reconnecting...'}
    </button>
  );
}

export default function App() {
  return (
    <>
      <SaveButton />
      <StatusBar />
    </>
  );
}

Убедитесь, что включение и выключение сети обновляет оба компонента.

Теперь у ваших компонентов нет такой повторяющейся логики. Что еще важнее, код внутри них описывает, что они хотят сделать (использовать статус подключения!), а не как это сделать (подписываясь на события браузера).

При извлечении логики в настраиваемые хуки можно скрыть сложные детали того, как вы работаете с какой-либо внешней системой или API браузера. Код ваших компонентов выражает ваше намерение, а не реализацию.

Имена хуков всегда начинаются с use

Приложения React построены из компонентов. Компоненты построены из хуков, встроенных или настраиваемых. Вы, вероятно, часто будете использовать настраиваемые хуки, созданные другими, но иногда вам может потребоваться написать свой собственный!

Вы должны следовать этим соглашениям об именовании:

  1. Имена компонентов React должны начинаться с заглавной буквы, например, StatusBar и SaveButton. Компоненты React также должны возвращать что-то, что React знает, как отображать, например, часть JSX.
  2. Имена хуков должны начинаться с use и заглавной буквы, например, useState (встроенный) или useOnlineStatus (настраиваемый, как ранее на странице). Хуки могут возвращать произвольные значения.

Это соглашение гарантирует, что вы всегда можете посмотреть на компонент и знать, где могут «скрываться» его состояние, эффекты и другие функции React. Например, если вы видите вызов функции getColor() внутри своего компонента, вы можете быть уверены, что она не может содержать состояние React внутри, потому что её имя не начинается с use. Однако вызов функции, такой как useOnlineStatus(), скорее всего, будет содержать вызовы других хуков внутри!

Примечание

Если ваш линтер настроен для React, он будет применять это соглашение об именовании. Прокрутите вверх к песочнице выше и переименуйте useOnlineStatus в getOnlineStatus. Обратите внимание, что линтер больше не позволит вам вызывать useState или useEffect внутри него. Только хуки и компоненты могут вызывать другие хуки!

Подробный разбор

Все ли функции, вызываемые во время отрисовки, должны начинаться с префикса use?

Нет. Функции, которые не вызывают хуков, не должны быть хуками.

Если ваша функция не вызывает никаких хуков, избегайте префикса use. Вместо этого напишите её как обычную функцию без префикса use. Например, useSorted ниже не вызывает хуков, поэтому назовите её getSorted:

// 🔴 Avoid: A Hook that doesn't use Hooks
function useSorted(items) {
  return items.slice().sort();
}

// ✅ Good: A regular function that doesn't use Hooks
function getSorted(items) {
  return items.slice().sort();
}

Это гарантирует, что вы можете вызвать эту обычную функцию где угодно, включая условные операторы:

function List({ items, shouldSort }) {
  let displayedItems = items;
  if (shouldSort) {
    // ✅ It's ok to call getSorted() conditionally because it's not a Hook
    displayedItems = getSorted(items);
  }
  // ...
}

Вы должны использовать префикс use для функции (и таким образом сделать её хуком), если она использует хотя бы один хук внутри:

// ✅ Good: A Hook that uses other Hooks
function useAuth() {
  return useContext(Auth);
}

Технически, это не навязывается React. В принципе, вы можете создать хук, который не вызывает другие хуки. Это часто запутывает и ограничивает, поэтому лучше избегать такой схемы. Однако могут быть редкие случаи, когда это полезно. Например, возможно, ваша функция сейчас не использует никаких хуков, но вы планируете добавить вызовы хуков в неё в будущем. Тогда имеет смысл назвать её с префиксом use:

// ✅ Good: A Hook that will likely use some other Hooks later
function useAuth() {
  // TODO: Replace with this line when authentication is implemented:
  // return useContext(Auth);
  return TEST_USER;
}

Тогда компоненты не смогут вызвать её условно. Это станет важным, когда вы фактически добавите вызовы хуков внутри. Если вы не планируете использовать хуки внутри неё (сейчас или позже), не делайте её хуком.

Настраиваемые хуки позволяют совместно использовать состоятельную логику, а не само состояние

В предыдущем примере, когда вы включали и выключали сеть, оба компонента обновлялись вместе. Однако неправильно думать, что одна переменная состояния isOnline совместно используется между ними. Посмотрите на этот код:

function StatusBar() {
  const isOnline = useOnlineStatus();
  // ...
}

function SaveButton() {
  const isOnline = useOnlineStatus();
  // ...
}

Он работает так же, как и до того, как вы извлекли дублирование:

function StatusBar() {
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    // ...
  }, []);
  // ...
}

function SaveButton() {
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    // ...
  }, []);
  // ...
}

Это две совершенно независимые переменные состояния и эффекты! У них случайно было одинаковое значение в одно и то же время, потому что вы синхронизировали их с одним и тем же внешним значением (включена ли сеть).

Чтобы лучше проиллюстрировать это, нам понадобится другой пример. Рассмотрим этот Form компонент:

import { useState } from 'react';

export default function Form() {
  const [firstName, setFirstName] = useState('Mary');
  const [lastName, setLastName] = useState('Poppins');

  function handleFirstNameChange(e) {
    setFirstName(e.target.value);
  }

  function handleLastNameChange(e) {
    setLastName(e.target.value);
  }

  return (
    <>
      <label>
        First name:
        <input value={firstName} onChange={handleFirstNameChange} />
      </label>
      <label>
        Last name:
        <input value={lastName} onChange={handleLastNameChange} />
      </label>
      <p><b>Good morning, {firstName} {lastName}.</b></p>
    </>
  );
}

Есть повторяющаяся логика для каждого поля формы:

  1. Часть состояния (firstName и lastName).
  2. Обработчик изменений (handleFirstNameChange и handleLastNameChange).
  3. Часть JSX, которая определяет атрибуты value и onChange для этого поля ввода.

Вы можете извлечь повторяющуюся логику в этот useFormInput настраиваемый хук:

import { useState } from 'react';

export function useFormInput(initialValue) {
  const [value, setValue] = useState(initialValue);

  function handleChange(e) {
    setValue(e.target.value);
  }

  const inputProps = {
    value: value,
    onChange: handleChange
  };

  return inputProps;
}

Обратите внимание, что он объявляет только одну переменную состояния, называемую value.

Однако, компонент Form вызывает useFormInput два раза:

function Form() {
  const firstNameProps = useFormInput('Mary');
  const lastNameProps = useFormInput('Poppins');
  // ...

Вот почему это работает так, как если бы вы объявляли две отдельные переменные состояния!

Настраиваемые хуки позволяют совместно использовать состоятельную логику, но не само состояние. Каждый вызов хука полностью независим от любого другого вызова того же хука. Именно поэтому две песочницы выше полностью эквивалентны. Если хотите, прокрутите вверх и сравните их. Поведение до и после извлечения настраиваемого хука идентично.

Если вам нужно совместно использовать само состояние между несколькими компонентами, поднимите его вверх и передавайте вниз вместо этого.

Передача реактивных значений между хуками

Код внутри ваших настраиваемых хуков будет повторно выполняться при каждом повторном рендеринге вашего компонента. Вот почему, как и компоненты, настраиваемые хуки должны быть чистыми. Подумайте о коде настраиваемых хуков как о части тела вашего компонента!

Поскольку настраиваемые хуки повторно рендерятся вместе с вашим компонентом, они всегда получают последние props и состояние. Чтобы увидеть, что это означает, рассмотрите этот пример чата. Измените URL сервера или чат-комнату:

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

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

  useEffect(() => {
    const options = {
      serverUrl: serverUrl,
      roomId: roomId
    };
    const connection = createConnection(options);
    connection.on('message', (msg) => {
      showNotification('New message: ' + msg);
    });
    connection.connect();
    return () => connection.disconnect();
  }, [roomId, serverUrl]);

  return (
    <>
      <label>
        Server URL:
        <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} />
      </label>
      <h1>Welcome to the {roomId} room!</h1>
    </>
  );
}

Когда вы меняете serverUrl или roomId, эффект “реагирует” на ваши изменения и повторно синхронизируется. Вы можете увидеть это по сообщениям консоли, что чат повторно подключается каждый раз, когда вы меняете зависимости эффекта.

Теперь переместите код эффекта в настраиваемый хук:

export function useChatRoom({ serverUrl, roomId }) {
  useEffect(() => {
    const options = {
      serverUrl: serverUrl,
      roomId: roomId
    };
    const connection = createConnection(options);
    connection.connect();
    connection.on('message', (msg) => {
      showNotification('New message: ' + msg);
    });
    return () => connection.disconnect();
  }, [roomId, serverUrl]);
}

Это позволяет вашему ChatRoom компоненту вызывать ваш настраиваемый хук, не беспокоясь о том, как он работает внутри:

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

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

  return (
    <>
      <label>
        Server URL:
        <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} />
      </label>
      <h1>Welcome to the {roomId} room!</h1>
    </>
  );
}

Это выглядит намного проще! (Но делает то же самое.)

Обратите внимание, что логика по-прежнему реагирует на изменения props и состояния. Попробуйте изменить URL сервера или выбранную комнату:

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

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

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

  return (
    <>
      <label>
        Server URL:
        <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} />
      </label>
      <h1>Welcome to the {roomId} room!</h1>
    </>
  );
}

Обратите внимание, как вы получаете возвращаемое значение одного хука:

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

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

и передаёте его как вход в другой хук:

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

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

Каждый раз, когда ваш ChatRoom компонент повторно рендерится, он передаёт последние roomId и serverUrl в ваш хук. Вот почему ваш эффект повторно подключается к чату всякий раз, когда их значения отличаются после повторного рендеринга. (Если вы когда-либо работали с программным обеспечением для обработки аудио или видео, цепочки хуков, подобные этой, могут напомнить вам о цепочках визуальных или аудиоэффектов. Это как если бы выход useState «подключался» к входу useChatRoom.)

Передача обработчиков событий в настраиваемые хуки

В разработке

Этот раздел описывает экспериментальный API, который ещё не выпущен в стабильной версии React.

При использовании useChatRoom в большем количестве компонентов, вам может потребоваться позволить компонентам настраивать его поведение. Например, в настоящее время логика того, что делать при получении сообщения, жёстко закодирована внутри обработчика:

export function useChatRoom({ serverUrl, roomId }) {
  useEffect(() => {
    const options = {
      serverUrl: serverUrl,
      roomId: roomId
    };
    const connection = createConnection(options);
    connection.connect();
    connection.on('message', (msg) => {
      showNotification('New message: ' + msg);
    });
    return () => connection.disconnect();
  }, [roomId, serverUrl]);
}

Предположим, вы хотите перенести эту логику обратно в свой компонент:

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

  useChatRoom({
    roomId: roomId,
    serverUrl: serverUrl,
    onReceiveMessage(msg) {
      showNotification('New message: ' + msg);
    }
  });
  // ...

Для этого измените свой пользовательский обработчик, чтобы он принимал onReceiveMessage в качестве одного из именованных параметров:

export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) {
  useEffect(() => {
    const options = {
      serverUrl: serverUrl,
      roomId: roomId
    };
    const connection = createConnection(options);
    connection.connect();
    connection.on('message', (msg) => {
      onReceiveMessage(msg);
    });
    return () => connection.disconnect();
  }, [roomId, serverUrl, onReceiveMessage]); // ✅ All dependencies declared
}

Это сработает, но есть ещё одно улучшение, которое можно сделать, когда ваш пользовательский обработчик принимает обработчики событий.

Добавление зависимости от onReceiveMessage не является идеальным вариантом, потому что это заставит чат повторно подключаться каждый раз, когда компонент перерисовывается. Оборатите этот обработчик события в событие эффекта, чтобы удалить его из зависимостей:

import { useEffect, useEffectEvent } from 'react';
// ...

export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) {
  const onMessage = useEffectEvent(onReceiveMessage);

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

Теперь чат не будет повторно подключаться каждый раз, когда компонент ChatRoom перерисовывается. Вот полностью рабочий демонстрационный пример передачи обработчика событий в пользовательский обработчик, с которым вы можете поработать:

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

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

  useChatRoom({
    roomId: roomId,
    serverUrl: serverUrl,
    onReceiveMessage(msg) {
      showNotification('New message: ' + msg);
    }
  });

  return (
    <>
      <label>
        Server URL:
        <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} />
      </label>
      <h1>Welcome to the {roomId} room!</h1>
    </>
  );
}

Обратите внимание, что вам больше не нужно знать, как useChatRoom работает, чтобы использовать его. Вы можете добавить его в любой другой компонент, передать любые другие параметры, и он будет работать одинаково. В этом и заключается сила пользовательских обработчиков.

Когда использовать пользовательские обработчики

Вам не нужно извлекать пользовательский обработчик для каждой небольшой дублирующейся части кода. Некоторые дублирования допустимы. Например, извлечение обработчика useFormInput для обёртки одного вызова useState, как раньше, вероятно, излишне.

Однако, каждый раз, когда вы пишете эффект, подумайте, не было бы ли яснее также обернуть его в пользовательский обработчик. Вам не нужно часто использовать эффекты, поэтому если вы пишете его, это означает, что вам нужно «выйти за рамки React», чтобы синхронизироваться с какой-то внешней системой или выполнить действие, для которого React не имеет встроенного API. Обертка в пользовательский обработчик позволяет точно передать ваше намерение и то, как данные передаются через него.

Например, рассмотрим компонент ShippingForm, который отображает два раскрывающихся списка: один показывает список городов, а другой — список районов в выбранном городе. Вы можете начать с кода, который выглядит так:

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

  const [city, setCity] = useState(null);
  const [areas, setAreas] = useState(null);
  // This Effect fetches areas for the selected city
  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]);

  // ...

Хотя этот код довольно повторяющийся, правильно сохранять эти эффекты раздельно друг от друга. Они синхронизируют две разные вещи, поэтому их не следует объединять в один эффект. Вместо этого вы можете упростить компонент ShippingForm выше, извлекая общую логику между ними в свой собственный обработчик useData:

function useData(url) {
  const [data, setData] = useState(null);
  useEffect(() => {
    if (url) {
      let ignore = false;
      fetch(url)
        .then(response => response.json())
        .then(json => {
          if (!ignore) {
            setData(json);
          }
        });
      return () => {
        ignore = true;
      };
    }
  }, [url]);
  return data;
}

Теперь вы можете заменить оба эффекта в компонентах ShippingForm вызовами useData:

function ShippingForm({ country }) {
  const cities = useData(`/api/cities?country=${country}`);
  const [city, setCity] = useState(null);
  const areas = useData(city ? `/api/areas?city=${city}` : null);
  // ...

Извлечение пользовательского обработчика делает поток данных явным. Вы передаёте url и получаете data. Скрывая ваш эффект внутри useData, вы также предотвращаете добавление кем-то, кто работает с компонентом ShippingForm, ненужных зависимостей к нему. Со временем большинство эффектов вашего приложения будут в пользовательских обработчиках.

Детальный разбор

Сосредоточьте свои пользовательские обработчики на конкретных задачах высокого уровня

Начните с выбора имени своего пользовательского обработчика. Если вам трудно выбрать понятное имя, это может означать, что ваш эффект слишком связан с остальной логикой вашего компонента и ещё не готов к извлечению.

В идеале, имя вашего пользовательского обработчика должно быть достаточно понятным, чтобы даже человек, который не пишет код, мог предположить, что делает ваш пользовательский обработчик, что он принимает и что возвращает:

  • ✅ useData(url)
  • ✅ useImpressionLog(eventName, extraData)
  • ✅ useChatRoom(options)

При синхронизации с внешней системой имя вашего пользовательского обработчика может быть более техническим и использовать терминологию, специфичную для этой системы. Это хорошо, если оно будет понятным для человека, знакомого с этой системой:

  • ✅ useMediaQuery(query)
  • ✅ useSocket(url)
  • ✅ useIntersectionObserver(ref, options)

Сосредоточьте пользовательские обработчики на конкретных задачах высокого уровня. Избегайте создания и использования пользовательских обработчиков «жизненного цикла», которые действуют как альтернативы и обёртки для самого API useEffect:

  • 🔴 useMount(fn)
  • 🔴 useEffectOnce(fn)
  • 🔴 useUpdateEffect(fn)

Например, этот обработчик useMount пытается обеспечить, чтобы некоторый код выполнялся только «при монтаже»:

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

  // 🔴 Avoid: using custom "lifecycle" Hooks
  useMount(() => {
    const connection = createConnection({ roomId, serverUrl });
    connection.connect();

    post('/analytics/event', { eventName: 'visit_chat' });
  });
  // ...
}

// 🔴 Avoid: creating custom "lifecycle" Hooks
function useMount(fn) {
  useEffect(() => {
    fn();
  }, []); // 🔴 React Hook useEffect has a missing dependency: 'fn'
}

Пользовательские обработчики «жизненного цикла», такие как useMount, плохо подходят для парадигмы React. Например, в этом примере кода есть ошибка (он не «реагирует» на изменения roomId или serverUrl), но линтер вас об этом не предупредит, так как линтер проверяет только прямые вызовы useEffect. Он не будет знать о вашем обработчике.

Если вы пишете эффект, начните с прямого использования API React:

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

  // ✅ Good: two raw Effects separated by purpose

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

  useEffect(() => {
    post('/analytics/event', { eventName: 'visit_chat', roomId });
  }, [roomId]);

  // ...
}

Затем (но необязательно) вы можете извлечь пользовательские обработчики для различных задач высокого уровня:

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

  // ✅ Great: custom Hooks named after their purpose
  useChatRoom({ serverUrl, roomId });
  useImpressionLog('visit_chat', { roomId });
  // ...
}

Хороший пользовательский обработчик делает вызываемый код более декларативным, ограничивая его действия. Например, useChatRoom(options) может только подключиться к чату, а useImpressionLog(eventName, extraData) может только отправить журнал впечатлений в аналитику. Если API вашего пользовательского обработчика не ограничивает варианты использования и очень абстрактен, в долгосрочной перспективе это, вероятно, приведёт к больше проблем, чем решает.

Пользовательские обработчики помогают вам перейти к лучшим шаблонам

Эффекты — это «спасительный выход»: вы используете их, когда вам нужно «выйти за рамки React», и когда для вашего случая использования нет лучшего встроенного решения. Со временем цель команды React — свести к минимуму количество эффектов в вашем приложении, предоставляя более специфичные решения для более специфических задач. Обертка ваших эффектов в пользовательские обработчики упрощает обновление вашего кода, когда такие решения становятся доступны.

Вернёмся к этому примеру:

import { useState, useEffect } from 'react';

export function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    function handleOnline() {
      setIsOnline(true);
    }
    function handleOffline() {
      setIsOnline(false);
    }
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);
  return isOnline;
}

В приведённом выше примере useOnlineStatus реализована парой useState и useEffect. Однако это не лучшее возможное решение. Оно не учитывает ряд краевых случаев. Например, оно предполагает, что при монтаже компонента isOnline уже true, но это может быть неправильно, если сеть уже отключилась. Вы можете использовать API браузера navigator.onLine, чтобы проверить это, но прямое использование не будет работать на сервере для генерации начального HTML. Короче говоря, этот код можно улучшить.

К счастью, React 18 включает в себя посвящённый API под названием useSyncExternalStore, который обрабатывает все эти проблемы за вас. Вот как ваш обработчик useOnlineStatus переписан с использованием этого нового API:

import { useSyncExternalStore } from 'react';

function subscribe(callback) {
  window.addEventListener('online', callback);
  window.addEventListener('offline', callback);
  return () => {
    window.removeEventListener('online', callback);
    window.removeEventListener('offline', callback);
  };
}

export function useOnlineStatus() {
  return useSyncExternalStore(
    subscribe,
    () => navigator.onLine, // How to get the value on the client
    () => true // How to get the value on the server
  );
}

Обратите внимание, что вам не нужно было изменять какие-либо компоненты, чтобы произвести эту миграцию:

function StatusBar() {
  const isOnline = useOnlineStatus();
  // ...
}

function SaveButton() {
  const isOnline = useOnlineStatus();
  // ...
}

Это ещё одна причина, по которой обёртка эффектов в пользовательские обработчики часто бывает выгодной:

  1. Вы делаете поток данных к эффектам и от них очень явным.
  2. Вы позволяете компонентам сосредоточиться на намерении, а не на точном выполнении ваших эффектов.
  3. При добавлении React новых функций вы можете удалить эти эффекты, не изменяя ни один из ваших компонентов.

Подобно системе дизайна, вам может быть полезно начать извлекать общие фразы из компонентов вашего приложения в пользовательские обработчики. Это позволит сохранить код компонентов сосредоточенным на намерении и позволит вам не писать эффекты очень часто. Многие отличные пользовательские обработчики поддерживаются сообществом React.

Детальный разбор

Предоставит ли React какое-либо встроенное решение для получения данных?

Мы всё ещё работаем над деталями, но ожидаем, что в будущем вы будете получать данные так:

import { use } from 'react'; // Not available yet!

function ShippingForm({ country }) {
  const cities = use(fetch(`/api/cities?country=${country}`));
  const [city, setCity] = useState(null);
  const areas = city ? use(fetch(`/api/areas?city=${city}`)) : null;
  // ...

Если вы используете пользовательские обработчики, такие как useData выше, в вашем приложении, для миграции к рекомендуемому подходу потребуется меньше изменений, чем если бы вы писали необработанные эффекты в каждом компоненте вручную. Однако старый подход по-прежнему будет работать, поэтому если вы чувствуете себя комфортно, работая с необработанными эффектами, вы можете продолжать делать это.

Существует более одного способа сделать это

Предположим, вы хотите реализовать анимацию плавного входа с нуля с помощью API браузера requestAnimationFrame. Вы можете начать с эффекта, который создаёт цикл анимации. В каждом кадре анимации вы можете изменить непрозрачность узла DOM, который вы сохраняете в ссылке, пока он не достигнет 1. Ваш код может начаться так:

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

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

  useEffect(() => {
    const duration = 1000;
    const node = ref.current;

    let startTime = performance.now();
    let frameId = null;

    function onFrame(now) {
      const timePassed = now - startTime;
      const progress = Math.min(timePassed / duration, 1);
      onProgress(progress);
      if (progress < 1) {
        // We still have more frames to paint
        frameId = requestAnimationFrame(onFrame);
      }
    }

    function onProgress(progress) {
      node.style.opacity = progress;
    }

    function start() {
      onProgress(0);
      startTime = performance.now();
      frameId = requestAnimationFrame(onFrame);
    }

    function stop() {
      cancelAnimationFrame(frameId);
      startTime = null;
      frameId = null;
    }

    start();
    return () => stop();
  }, []);

  return (
    <h1 className="welcome" ref={ref}>
      Welcome
    </h1>
  );
}

export default function App() {
  const [show, setShow] = useState(false);
  return (
    <>
      <button onClick={() => setShow(!show)}>
        {show ? 'Remove' : 'Show'}
      </button>
      <hr />
      {show && <Welcome />}
    </>
  );
}

Чтобы сделать компонент более читаемым, вы можете извлечь логику в пользовательский обработчик useFadeIn:

import { useState, useEffect, useRef } from 'react';
import { useFadeIn } from './useFadeIn.js';

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

  useFadeIn(ref, 1000);

  return (
    <h1 className="welcome" ref={ref}>
      Welcome
    </h1>
  );
}

export default function App() {
  const [show, setShow] = useState(false);
  return (
    <>
      <button onClick={() => setShow(!show)}>
        {show ? 'Remove' : 'Show'}
      </button>
      <hr />
      {show && <Welcome />}
    </>
  );
}

Вы могли бы сохранить код useFadeIn таким же, но также можете его переработать. Например, вы можете извлечь логику настройки цикла анимации из useFadeIn в пользовательский обработчик useAnimationLoop:

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

export function useFadeIn(ref, duration) {
  const [isRunning, setIsRunning] = useState(true);

  useAnimationLoop(isRunning, (timePassed) => {
    const progress = Math.min(timePassed / duration, 1);
    ref.current.style.opacity = progress;
    if (progress === 1) {
      setIsRunning(false);
    }
  });
}

function useAnimationLoop(isRunning, drawFrame) {
  const onFrame = useEffectEvent(drawFrame);

  useEffect(() => {
    if (!isRunning) {
      return;
    }

    const startTime = performance.now();
    let frameId = null;

    function tick(now) {
      const timePassed = now - startTime;
      onFrame(timePassed);
      frameId = requestAnimationFrame(tick);
    }

    tick();
    return () => cancelAnimationFrame(frameId);
  }, [isRunning]);
}

Однако, вам не нужно этого делать. Как и с обычными функциями, в конечном итоге вы решаете, где провести границы между разными частями вашего кода. Вы также можете использовать совершенно другой подход. Вместо того, чтобы хранить логику в эффекте, вы можете переместить большую часть императивной логики внутри класса JavaScript :

import { useState, useEffect } from 'react';
import { FadeInAnimation } from './animation.js';

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

Эффекты позволяют соединять React с внешними системами. Чем больше требуется координации между эффектами (например, для объединения нескольких анимаций), тем более целесообразно извлечь эту логику из эффектов и обработчиков полностью, как в примере выше. Затем извлечённый код становится «внешней системой». Это позволяет вашим эффектам оставаться простыми, так как им нужно только отправлять сообщения в систему, которую вы вывели за пределы React.

Приведенные выше примеры предполагают, что логика плавного появления должна быть реализована на JavaScript. Однако данная анимация плавного появления проще и эффективнее реализуется с помощью обычной CSS-анимации:

.welcome {
  color: white;
  padding: 50px;
  text-align: center;
  font-size: 50px;
  background-image: radial-gradient(circle, rgba(63,94,251,1) 0%, rgba(252,70,107,1) 100%);

  animation: fadeIn 1000ms;
}

@keyframes fadeIn {
  0% { opacity: 0; }
  100% { opacity: 1; }
}

Иногда вам даже не нужен хук!

Резюме

  • Пользовательские хуки позволяют совместно использовать логику между компонентами.
  • Пользовательские хуки должны иметь имя, начинающееся с use и за которым следует заглавная буква.
  • Пользовательские хуки только совместно используют поведенческую логику, а не само состояние.
  • Вы можете передавать реактивные значения от одного хука к другому, и они останутся актуальными.
  • Все хуки перевыполняются каждый раз, когда ваш компонент перерисовывается.
  • Код ваших пользовательских хуков должен быть чистым, как и код вашего компонента.
  • Оборачивайте обработчики событий, полученные пользовательскими хуками, в события эффектов.
  • Не создавайте пользовательские хуки, такие как useMount. Поддерживайте их цель конкретной.
  • Как и где определять границы вашего кода, решаете вы сами.

Попробуйте выполнить несколько заданий

Задание 1 из 5:
Извлечь useCounter хук

Этот компонент использует переменную состояния и эффект для отображения числа, которое увеличивается каждую секунду. Извлеките эту логику в пользовательский хук, называемый useCounter. Ваша цель – добиться того, чтобы реализация компонента Counter выглядела точно так же:

export default function Counter() {
  const count = useCounter();
  return <h1>Seconds passed: {count}</h1>;
}

Вам нужно написать свой пользовательский хук в useCounter.js и импортировать его в файл App.js.

import { useState, useEffect } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);
  return <h1>Seconds passed: {count}</h1>;
}

© 2013–present Facebook Inc.
Licensed under the Creative Commons Attribution 4.0 International Public License.
https://react.dev/learn/reusing-logic-with-custom-hooks

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API