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, в React есть встроенный хук 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');
  // ...

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

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

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

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

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

Поскольку пользовательские хуки повторно выполняются вместе с компонентом, они всегда получают актуальные пропсы и состояние. Чтобы понять, что это значит, рассмотрим пример с чатом. Измените 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>
    </>
  );
}

Выглядит намного проще! (Но работает так же.)

Обратите внимание: логика по-прежнему реагирует на изменения пропсов и состояния. Попробуйте изменить 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.)

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

Когда вы начнете использовать 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 есть специальный 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();
  // ...
}

Это ещё одна причина, по которой часто полезно оборачивать Effects в пользовательские Hooks:

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

Подобно дизайн-системе, вы можете счесть полезным выделить распространённые шаблоны из компонентов приложения в пользовательские Hooks. Так код компонентов будет сосредоточен на цели, а вам не придётся часто писать Effects напрямую. Сообщество React поддерживает множество отличных пользовательских Hooks.

Подробнее

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

Сегодня с помощью API use данные можно считывать во время рендеринга, передав в use значение типа Promise:

import { use, Suspense } from "react";

function Message({ messagePromise }) {
  const messageContent = use(messagePromise);
  return <p>Here is the message: {messageContent}</p>;
}

export function MessageContainer({ messagePromise }) {
  return (
    <Suspense fallback={<p>⌛Downloading message...</p>}>
      <Message messagePromise={messagePromise} />
    </Suspense>
  );
}

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

import { use } from 'react';

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;
  // ...

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

Есть несколько способов это сделать

Предположим, вы хотите реализовать анимацию плавного появления с нуля, используя API браузера requestAnimationFrame. Можно начать с Effect, который запускает цикл анимации. На каждом кадре анимации можно изменять прозрачность DOM-узла, который вы храните в ref, пока она не достигнет значения 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 { 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]);
}

Однако делать это не обязательно. Как и в случае с обычными функциями, в конечном счёте вы сами решаете, где провести границы между различными частями кода. Можно выбрать и совершенно другой подход. Вместо того чтобы оставлять логику в Effect, можно перенести большую часть императивной логики в класс 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]);
}

Effects позволяют связывать React с внешними системами. Чем больше координации требуется между Effects (например, чтобы объединить несколько анимаций в цепочку), тем разумнее полностью вынести эту логику за пределы Effects и Hooks, как в примере выше. Тогда извлечённый код становится «внешней системой». Благодаря этому Effects остаются простыми: им нужно лишь отправлять сообщения системе, которую вы вынесли за пределы 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; }
}

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

Итоги

  • Пользовательские Hooks позволяют совместно использовать логику между компонентами.
  • Имена пользовательских Hooks должны начинаться с use, за которой следует заглавная буква.
  • Пользовательские Hooks позволяют совместно использовать только логику с состоянием, но не само состояние.
  • Можно передавать реактивные значения из одного Hook в другой — они будут оставаться актуальными.
  • Все Hooks запускаются повторно при каждом повторном рендеринге компонента.
  • Код пользовательских Hooks, как и код компонентов, должен быть чистым.
  • Оборачивайте обработчики событий, полученные пользовательскими Hooks, в Effect Events.
  • Не создавайте пользовательские Hooks вроде useMount. Формулируйте для них конкретную цель.
  • Вы сами решаете, как и где проводить границы между частями кода.

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

Задание 1 из 5:
Вынесите хук useCounter

Этот компонент использует переменную состояния и Effect, чтобы отображать число, увеличивающееся каждую секунду. Вынесите эту логику в пользовательский хук с именем 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