Noveo

Наш блог Как избежать состояния гонки и утечки памяти при использовании React useEffect

Как избежать состояния гонки и утечки памяти при использовании React useEffect

Используете React useEffect и сталкиваетесь с состоянием гонки и утечки памяти? Возможно, перевод поста Саранша Катарии (Saransh Kataria) поможет вам этого избежать!

React useEffect

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

 

import React, { useEffect} from 'react';
export default function UseEffectWithRaceCondition() {
  const [todo, setTodo] = useState(null);
  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');
      const newData = await response.json();
      setTodo(newData);
    };
    fetchData();
  }, []);
  if (data) {
    return <div>{data.title}</div>;
  } else {
    return null;
  }
}

Мы установили пустой массив в качестве зависимости на использование useEffect React hook. Мы убедились, что запрос fetch срабатывает только раз. Но в данном компоненте есть вероятность получения состояния гонки и утечки памяти. Почему?

 

Утечка памяти происходит, если у API-сервера уходит некоторое время на ответ, и компонент демонтируется до того, как этот ответ получен. Несмотря на то, что компонент был демонтирован, мы всё равно получим и обработаем ответ сервера. Впоследствии ответ парсится, и будет вызван setTodo. В этом случае React нас предупредит:

 

Невозможно обновить состояние демонтированного компонента. Это no-op, но означает утечку памяти в вашем приложении. Чтобы это исправить, отмените все подписки и асинхронные задачи в функции useEffect cleanup.

 

И это сообщение достаточно понятно.

 

Другой возможный сценарий с подобной проблемой мог бы произойти в случае, если компонент принимает todo Id как prop.

import React, { useEffect} from 'react';
export default function UseEffectWithRaceCondition( {id} ) {
  const [todo, setTodo] = useState(null);
  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`);
      const newData = await response.json();
      setTodo(newData);
    };
    fetchData();
  }, [id]);
  if (data) {
    return <div>{data.title}</div>;
  } else {
    return null;
  }
}

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

Возможные решения для исправления проблемы

Есть несколько способов решения этой проблемы. Здесь мы расскажем два из них, оба используют преимущества функции cleanup, которую предоставляет useEffect.

  • Можно использовать булевый флаг, чтобы убедиться, что компонент смонтирован. Таким образом мы обновляем состояние, если флаг является правдивым. Если мы делаем множественные запросы внутри компонента, то мы будем видеть информацию по последнему запросу.
  • Можно использовать AbortController, чтобы отменить предыдущий запрос в любой момент, когда демонтируется компонент. Но  AbortController  не поддерживается в IE. Важно об этом помнить, если будете применять этот подход.

Использование useEffect cleanup с булевыми флагами

useEffect(() => {
  let isComponentMounted = true;
    const fetchData = async () => {
      const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');
      const newData = await response.json();
      if(isComponentMounted) {
        setTodo(newData);
      }
    };
    fetchData();
    return () => {
      isComponentMounted = false;
    }
  }, []);

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

 

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

Использование useEffect cleanup с AbortController

Предыдущий способ работает,  но не является лучшим решением проблемы состояния гонки фоновых запросов. Фоновые устаревшие запросы создают ненужное потребление трафика. Браузер также ограничивает максимальное количество совпадающих по времени запросов (максимально 6-8).

Из нашего предыдущего поста о том, как отменить запрос HTTP fetch, мы знаем о AbortController API, который был добавлен в DOM-стандарт. Мы можем им воспользоваться, чтобы отменить лишние запросы.

 

useEffect(() => {
  let abortController = new AbortController();
    const fetchData = async () => {
      try {
        const response = await fetch('https://jsonplaceholder.typicode.com/todos/1', {
            signal: abortController.signal,
          });
      const newData = await response.json();
        setTodo(newData);
      }
      catch(error) {
         if (error.name === 'AbortError') {
          // Handling error thrown by aborting request
        }
      }
    };
    fetchData();
    return () => {
      abortController.abort();
    }
  }, []);

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

 

Это решение работает аналогично предыдущему. В случае повторного рендеринга функция cleanup исполняется до того, как исполнится следующий эффект. Отличие в том, что браузер также отменяет запрос (для этого мы и используем AbortController).

 

При помощи этих двух способов мы можем избежать состояния гонки при отправке API-запросов, используя useEffect hook в React. Если вы хотите использовать сторонние библиотеки с возможностью отмены запросов, используйте Axios или react query, они также предоставят множество других фич.

 

Оригинал: https://www.wisdomgeek.com/development/web-development/react/avoiding-race-conditions-memory-leaks-react-useeffect/

Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.

НазадПредыдущий пост ВпередСледующий пост

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: