Spec-Zone.ru › React 17

Использование хука Effect

Хуки — это новое дополнение в React 16.8. Они позволяют использовать состояние и другие функции React без написания класса.

Хук Effect позволяет выполнять побочные эффекты в функциональных компонентах:

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

function Example() {
  const [count, setCount] = useState(0);

  // Similar to componentDidMount and componentDidUpdate:
  useEffect(() => {
    // Update the document title using the browser API
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

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

Получение данных, настройка подписки и ручное изменение DOM в компонентах React — всё это примеры побочных эффектов. Вам могут быть знакомы эти операции как «побочные эффекты» (или просто «эффекты»), но вы, скорее всего, выполняли их в своих компонентах раньше.

Подсказка

Если вы знакомы с методами жизненного цикла классов React, вы можете рассматривать хук useEffect как комбинацию componentDidMount, componentDidUpdate и componentWillUnmount.

Существует два распространённых типа побочных эффектов в компонентах React: те, которые не требуют очистки, и те, которые требуют.

Эффекты без очистки

Иногда мы хотим выполнить дополнительный код после того, как React обновил DOM. Сеть запросы, ручные изменения DOM и логирование — это распространённые примеры эффектов, которые не требуют очистки. Мы говорим это потому, что мы можем их выполнить и сразу забыть о них. Давайте сравним, как классы и хуки позволяют выражать такие побочные эффекты.

Пример с использованием классов

В компонентах React с классами сам метод render не должен вызывать побочные эффекты. Это будет слишком рано — мы обычно хотим выполнить наши эффекты после того, как React обновил DOM.

Вот почему в классах React мы помещаем побочные эффекты в componentDidMount и componentDidUpdate. Вернувшись к нашему примеру, вот компонент счётчика React с классом, который обновляет заголовок документа сразу после того, как React внес изменения в DOM:

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
  }

  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

Обратите внимание, как нам нужно дублировать код между этими двумя методами жизненного цикла в классе.

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

Теперь давайте посмотрим, как мы можем сделать то же самое с хуком useEffect.

Пример с использованием хуков

Мы уже видели этот пример в начале этой страницы, но давайте рассмотрим его подробнее:

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

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

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

Почему useEffect вызывается внутри компонента? Размещение useEffect внутри компонента позволяет нам получить доступ к переменной состояния count (или любым свойствам) прямо из эффекта. Нам не нужен специальный API для его чтения — он уже находится в области видимости функции. Хуки используют замыкания JavaScript и избегают введения React-специфичных API, когда JavaScript уже предоставляет решение.

useEffect выполняется после каждого рендера? Да! По умолчанию он выполняется как после первого рендера, так и после каждого обновления. (Позже мы поговорим о как настроить это.) Вместо того, чтобы думать в терминах «монтирования» и «обновления», вы можете найти проще думать, что эффекты происходят «после рендера». React гарантирует, что DOM обновлён к тому времени, когда он запускает эффекты.

Подробное объяснение

Теперь, когда мы больше знаем об эффектах, эти строки должны быть понятны:

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });
}

Мы объявляем переменную состояния count, а затем говорим React, что нам нужно использовать эффект. Мы передаём функцию хуку useEffect. Эта переданная функция есть наш эффект. Внутри эффекта мы устанавливаем заголовок документа, используя API браузера document.title. Мы можем получить доступ к последнему значению count внутри эффекта, потому что оно находится в области видимости нашей функции. Когда React рендерит наш компонент, он запомнит используемый эффект и выполнит его после обновления DOM. Это происходит при каждом рендере, включая первый.

Опытные разработчики JavaScript могут заметить, что функция, переданная в useEffect, будет разной при каждом рендере. Это сделано намеренно. На самом деле, это позволяет нам получать значение count изнутри эффекта, не беспокоясь о том, что оно устареет. Каждый раз при повторном рендере мы планируем разный эффект, заменяя предыдущий. В некотором смысле, это заставляет эффекты вести себя более как часть результата рендера — каждый эффект «принадлежит» конкретному рендеру. Мы более подробно рассмотрим, почему это полезно, позже на этой странице.

Подсказка

В отличие от componentDidMount или componentDidUpdate, эффекты, запланированные с помощью useEffect, не блокируют браузер от обновления экрана. Это делает ваше приложение более отзывчивым. Большинству эффектов не нужно происходить синхронно. В редких случаях, когда это необходимо (например, измерение макета), есть отдельный хук useLayoutEffect с API, идентичным useEffect.

Эффекты с очисткой

Ранее мы рассматривали, как выражать побочные эффекты, которые не требуют очистки. Однако некоторые эффекты требуют. Например, мы можем захотеть настроить подписку на какой-либо внешний источник данных. В этом случае важно очистить подписку, чтобы не создать утечку памяти! Давайте сравним, как это можно сделать с классами и хуками.

Пример с использованием классов

В компоненте React с классом вы обычно настраиваете подписку в componentDidMount, и очищаете её в componentWillUnmount. Например, предположим, что у нас есть модуль ChatAPI, который позволяет нам подписаться на онлайн-статус друга. Вот как мы можем подписаться и отобразить этот статус с использованием класса:

class FriendStatus extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }

  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  handleStatusChange(status) {
    this.setState({
      isOnline: status.isOnline
    });
  }

  render() {
    if (this.state.isOnline === null) {
      return 'Loading...';
    }
    return this.state.isOnline ? 'Online' : 'Offline';
  }
}

Обратите внимание, как componentDidMount и componentWillUnmount должны зеркалить друг друга. Методы жизненного цикла заставляют нас разделить этот код, даже если по смыслу код в обоих методах относится к одному эффекту.

Примечание

Внимательные читатели могут заметить, что для полной корректности этому примеру также нужен метод componentDidUpdate. Мы пока проигнорируем это, но вернёмся к этому в позже на этой странице.

Пример с использованием хуков

Давайте посмотрим, как мы можем написать этот компонент с помощью хуков.

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

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

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    // Specify how to clean up after this effect:
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

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

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

Примечание

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

Резюме

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

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

Другие эффекты могут не иметь фазы очистки и ничего не возвращают.

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

Хук Effect объединяет оба случая с единым API.

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

Советы по использованию эффектов

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

Совет: использование нескольких эффектов для разделения задач

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

class FriendStatusWithCounter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0, isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  handleStatusChange(status) {
    this.setState({
      isOnline: status.isOnline
    });
  }
  // ...

Обратите внимание, как логика установки document.title разделена между componentDidMount и componentDidUpdate. Логика подписки также разделена между componentDidMount и componentWillUnmount. А componentDidMount содержит код для обеих задач.

Итак, как хуки могут решить эту проблему? Так же, как вы можете использовать хук State несколько раз, вы также можете использовать несколько эффектов. Это позволяет нам разделить не связанную логику на разные эффекты:

function FriendStatusWithCounter(props) {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });
  // ...
}

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

Объяснение: почему эффекты выполняются при каждом обновлении

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

Ранее на этой странице мы представили пример компонента FriendStatus, который отображает, онлайн ли друг или нет. Наш класс считывает friend.id из this.props, подписывается на статус друга после монтирования компонента и отписывается при размонтировании:

  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

Но что произойдёт, если свойство friend изменится во время отображения компонента на экране? Наш компонент будет продолжать отображать онлайн-статус другого друга. Это ошибка. Мы также можем вызвать утечку памяти или сбой при размонтировании, так как вызов unsubscribe будет использовать неправильный идентификатор друга.

В компоненте класса нам нужно добавить componentDidUpdate для обработки этого случая:

  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentDidUpdate(prevProps) {
    // Unsubscribe from the previous friend.id
    ChatAPI.unsubscribeFromFriendStatus(
      prevProps.friend.id,
      this.handleStatusChange
    );
    // Subscribe to the next friend.id
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

Забывание о правильной обработке componentDidUpdate — распространённая причина ошибок в приложениях React.

Теперь рассмотрим версию этого компонента, использующего хуки:

function FriendStatus(props) {
  // ...
  useEffect(() => {
    // ...
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

Он не страдает от этой ошибки. (Но мы также не внесли в него никаких изменений.)

Нет специального кода для обработки обновлений, потому что useEffect обрабатывает их по умолчанию. Он очищает предыдущие эффекты перед применением последующих. Чтобы проиллюстрировать это, вот последовательность вызовов subscribe и unsubscribe, которые этот компонент может производить со временем:

// Mount with { friend: { id: 100 } } props
ChatAPI.subscribeToFriendStatus(100, handleStatusChange);     // Run first effect

// Update with { friend: { id: 200 } } props
ChatAPI.unsubscribeFromFriendStatus(100, handleStatusChange); // Clean up previous effect
ChatAPI.subscribeToFriendStatus(200, handleStatusChange);     // Run next effect

// Update with { friend: { id: 300 } } props
ChatAPI.unsubscribeFromFriendStatus(200, handleStatusChange); // Clean up previous effect
ChatAPI.subscribeToFriendStatus(300, handleStatusChange);     // Run next effect

// Unmount
ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange); // Clean up last effect

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

Совет: оптимизация производительности путём пропуска эффектов

В некоторых случаях очистка или применение эффекта после каждого рендера может создать проблему производительности. В компонентах класса это можно решить, написав дополнительное сравнение с prevProps или prevState внутри componentDidUpdate:

componentDidUpdate(prevProps, prevState) {
  if (prevState.count !== this.state.count) {
    document.title = `You clicked ${this.state.count} times`;
  }
}

Это требование достаточно распространено, чтобы быть встроенным в API хука useEffect. Вы можете сказать React, чтобы пропустил применение эффекта, если определённые значения не изменились между перерисовками. Для этого передайте массив в качестве необязательного второго аргумента хуку useEffect:

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes

В приведённом выше примере мы передаём [count] в качестве второго аргумента. Что это означает? Если count имеет значение 5, а затем наш компонент перерисовывается со значением count, всё ещё равным 5, React сравнит [5] с предыдущего рендера и [5] с последующего рендера. Поскольку все элементы массива одинаковы (5 === 5), React пропустит эффект. Это наша оптимизация.

Когда мы рендерим с обновлённым значением count до 6, React сравнит элементы массива [5] с предыдущего рендера с элементами массива [6] следующего рендера. На этот раз React повторно применит эффект, потому что 5 !== 6. Если в массиве несколько элементов, React повторно выполнит эффект, даже если только один из них отличается.

Это также работает для эффектов, имеющих фазу очистки:

useEffect(() => {
  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
  return () => {
    ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
  };
}, [props.friend.id]); // Only re-subscribe if props.friend.id changes

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

Примечание

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

Если вы хотите выполнить эффект и очистить его только один раз (при монтаже и размонтировании), вы можете передать пустой массив ([]) в качестве второго аргумента. Это указывает React, что ваш эффект не зависит от никаких значений из свойств или состояния, поэтому ему никогда не придётся его повторно запускать. Это не обрабатывается как особый случай — это напрямую следует из того, как всегда работает массив зависимостей.

Если вы передадите пустой массив ([]), свойства и состояние внутри эффекта всегда будут иметь свои начальные значения. Хотя передача [] в качестве второго аргумента ближе к знакомому componentDidMount и componentWillUnmount ментальному образу, обычно есть лучшие решения, чтобы избежать слишком частых повторных запусков эффектов. Также не забывайте, что React откладывает выполнение useEffect до момента, когда браузер выполнит отрисовку, поэтому дополнительная работа представляет меньшую проблему.

Мы рекомендуем использовать правило exhaustive-deps как часть нашего пакета eslint-plugin-react-hooks. Оно предупреждает о неправильно указанных зависимостях и предлагает исправление.

Следующие шаги

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

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

На этом этапе вы можете задаться вопросом, как работают хуки. Как React может узнать, какой вызов useState соответствует какой переменной состояния между перерисовками? Как React «сопоставляет» предыдущие и последующие эффекты при каждом обновлении? На следующей странице мы изучим Правила хуков — они необходимы для работы хуков.

Полезная ли эта страница?

© 2013–present Facebook Inc.
Licensed under the Creative Commons Attribution 4.0 International Public License.
https://17.reactjs.org/docs/hooks-effect.html

Spec-Zone.ru

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