Предупреждения React Hook для асинхронной функции в useEffect: функция useEffect должна возвращать функцию очистки или ничего

142

Я пробовал useEffectпример ниже:

useEffect(async () => {
    try {
        const response = await fetch(`https://www.reddit.com/r/${subreddit}.json`);
        const json = await response.json();
        setPosts(json.data.children.map(it => it.data));
    } catch (e) {
        console.error(e);
    }
}, []);

и я получаю это предупреждение в своей консоли. Но я думаю, что очистка необязательна для асинхронных вызовов. Не знаю, почему я получаю это предупреждение. Связывание песочницы для примеров. https://codesandbox.io/s/24rj871r0p введите описание изображения здесь

RedPandaz
источник

Ответы:

199

Предлагаю взглянуть на ответ Дэна Абрамова (одного из разработчиков ядра React) :

Я думаю, вы усложняете это дело, чем должно быть.

function Example() {
  const [data, dataSet] = useState<any>(null)

  useEffect(() => {
    async function fetchMyAPI() {
      let response = await fetch('api/data')
      response = await response.json()
      dataSet(response)
    }

    fetchMyAPI()
  }, [])

  return <div>{JSON.stringify(data)}</div>
}

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

const response = MyAPIResource.read();

и никаких эффектов. Но пока вы можете переместить асинхронный материал в отдельную функцию и вызвать ее.

Вы можете узнать больше об экспериментальном саспенсе здесь .


Если вы хотите использовать внешние функции с eslint.

 function OutsideUsageExample() {
  const [data, dataSet] = useState<any>(null)

  const fetchMyAPI = useCallback(async () => {
    let response = await fetch('api/data')
    response = await response.json()
    dataSet(response)
  }, [])

  useEffect(() => {
    fetchMyAPI()
  }, [fetchMyAPI])

  return (
    <div>
      <div>data: {JSON.stringify(data)}</div>
      <div>
        <button onClick={fetchMyAPI}>manual fetch</button>
      </div>
    </div>
  )
}

С useCallback useCallback . Песочница .

import React, { useState, useEffect, useCallback } from "react";

export default function App() {
  const [counter, setCounter] = useState(1);

  // if counter is changed, than fn will be updated with new counter value
  const fn = useCallback(() => {
    setCounter(counter + 1);
  }, [counter]);

  // if counter is changed, than fn will not be updated and counter will be always 1 inside fn
  /*const fnBad = useCallback(() => {
      setCounter(counter + 1);
    }, []);*/

  // if fn or counter is changed, than useEffect will rerun
  useEffect(() => {
    if (!(counter % 2)) return; // this will stop the loop if counter is not even

    fn();
  }, [fn, counter]);

  // this will be infinite loop because fn is always changing with new counter value
  /*useEffect(() => {
    fn();
  }, [fn]);*/

  return (
    <div>
      <div>Counter is {counter}</div>
      <button onClick={fn}>add +1 count</button>
    </div>
  );
}
ZiiMakc
источник
Вы можете решить проблемы, связанные с состоянием гонки, проверив, useEffect(() => { let unmounted = false promise.then(res => { if (!unmounted) { setState(...) } }) return () => { unmounted = true } }, [])
Ричард,
1
Вы также можете использовать пакет с именем use-async-effect . Этот пакет позволяет использовать синтаксис async await.
KittyCat
Использование функции самовызова, не позволяющей асинхронному процессу просачиваться в определение функции useEffect, или настраиваемая реализация функции, которая запускает асинхронный вызов в качестве оболочки вокруг useEffect, являются лучшим вариантом на данный момент. Хотя вы можете включить новый пакет, такой как предлагаемый use-async-effect, я думаю, что это простая проблема.
Thulani Chivandikwa
2
эй, это нормально, и чем я занимаюсь чаще всего. но eslintпросит меня сделать fetchMyAPI()как зависимость отuseEffect
Пракаш Редди Потлападу
52

Когда вы используете асинхронную функцию, например

async () => {
    try {
        const response = await fetch(`https://www.reddit.com/r/${subreddit}.json`);
        const json = await response.json();
        setPosts(json.data.children.map(it => it.data));
    } catch (e) {
        console.error(e);
    }
}

он возвращает обещание и useEffectне ожидает, что функция обратного вызова вернет обещание, скорее он ожидает, что ничего не будет возвращено или функция будет возвращена.

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

useEffect(() => {
    (async function() {
        try {
            const response = await fetch(
                `https://www.reddit.com/r/${subreddit}.json`
            );
            const json = await response.json();
            setPosts(json.data.children.map(it => it.data));
        } catch (e) {
            console.error(e);
        }
    })();
}, []);

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

useEffect(() => {
    async function fetchData() {
        try {
            const response = await fetch(
                `https://www.reddit.com/r/${subreddit}.json`
            );
            const json = await response.json();
            setPosts(json.data.children.map(it => it.data));
        } catch (e) {
            console.error(e);
        }
    };
    fetchData();
}, []);

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

Рабочие коды

Шубхам Хатри
источник
Был создан пакет, упрощающий эту задачу. Вы можете найти это здесь .
KittyCat
1
но eslint не потерпит с этим
Мухаймин CS
1
нет возможности выполнить обратный вызов cleanup / didmount
Дэвид Рирт
1
@ShubhamKhatri, когда вы используете, useEffectвы можете вернуть функцию для очистки, например, для отмены подписки на события. Когда вы используете функцию async, вы не можете ничего вернуть, потому useEffectчто не будете ждать результата,
Дэвид Рирте,
2
Вы говорите, что я могу добавить функцию очистки в асинхронную? Я пробовал, но моя функция очистки никогда не вызывается. Вы можете привести небольшой пример?
Дэвид Реарте
33

Пока React не предоставит лучший способ, вы можете создать помощника useEffectAsync.js:

import { useEffect } from 'react';


export default function useEffectAsync(effect, inputs) {
    useEffect(() => {
        effect();
    }, inputs);
}

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

useEffectAsync(async () => {
    const items = await fetchSomeItems();
    console.log(items);
}, []);
Эд я
источник
9
Причина, по которой React не позволяет автоматически использовать асинхронные функции в useEffect, заключается в том, что в огромной части случаев требуется некоторая очистка. Функция в том useAsyncEffectвиде, в котором вы ее написали, может легко ввести кого-то в заблуждение, если он вернет функцию очистки из своего асинхронного эффекта, она будет запущена в подходящее время. Это могло привести к утечкам памяти или еще более серьезным ошибкам, поэтому мы решили побудить людей провести рефакторинг своего кода, чтобы сделать «шов» асинхронных функций, взаимодействующих с жизненным циклом React, более заметным, а поведение кода в результате, надеюсь, более осознанным и правильным.
Софи Альперт,
8

Я прочитал этот вопрос и считаю, что лучший способ реализовать useEffect не упоминается в ответах. Допустим, у вас есть сетевой вызов, и вы хотите что-то предпринять, когда получите ответ. Для простоты сохраним ответ сети в переменной состояния. Можно использовать действие / редуктор для обновления хранилища ответом сети.

const [data, setData] = useState(null);

/* This would be called on initial page load */
useEffect(()=>{
    fetch(`https://www.reddit.com/r/${subreddit}.json`)
    .then(data => {
        setData(data);
    })
    .catch(err => {
        /* perform error handling if desired */
    });
}, [])

/* This would be called when store/state data is updated */
useEffect(()=>{
    if (data) {
        setPosts(data.children.map(it => {
            /* do what you want */
        }));
    }
}, [data]);

Ссылка => https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects

Chiranjib
источник
4

Для других читателей ошибка может возникать из-за отсутствия скобок, заключающих асинхронную функцию:

Учитывая асинхронную функцию initData

  async function initData() {
  }

Этот код приведет к вашей ошибке:

  useEffect(() => initData(), []);

Но этот не будет:

  useEffect(() => { initData(); }, []);

(Обратите внимание на скобки вокруг initData ()

Саймон
источник
1
Молодец, чувак! Я использую сагу, и эта ошибка появилась, когда я вызвал создателя действия, который вернул единственный объект. Похоже, что callback-функция useEffect не устраняет такое поведение. Я ценю твой ответ.
Gorr1995
2
На всякий случай, если люди задаются вопросом, почему это правда ... Без фигурных скобок возвращаемое значение initData () неявно возвращается стрелочной функцией. С фигурными скобками ничего не возвращается неявно, поэтому ошибки не произойдет.
Marnix.hoh
4

Здесь можно использовать оператор void .
Вместо того:

React.useEffect(() => {
    async function fetchData() {
    }
    fetchData();
}, []);

или

React.useEffect(() => {
    (async function fetchData() {
    })()
}, []);

можно было написать:

React.useEffect(() => {
    void async function fetchData() {
    }();
}, []);

Он немного чище и красивее.


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

function App() {
  const [data, setData] = React.useState([])

  React.useEffect(() => {
    const abortController = new AbortController()
    ;(async function fetchData() {
      try {
        const url = "https://jsonplaceholder.typicode.com/todos/1"
        const response = await fetch(url, {
          signal: abortController.signal,
        })
        setData(await response.json())
      } catch (error) {
        console.log("error", error)
      }
    })()
    return abortController.abort // cancel pending fetch request on component unmount
  }, [])

  return <pre>{JSON.stringify(data, null, 2)}</pre>
}

Кайкал
источник
Вы знаете свой JS, сэр. AbortController - это новая штука, которая доступна после того, как предложение об отмене обещаний не удалось
Девин Дж. Род,
Кстати, есть пакет «use-abortable-fetch», но я не уверен, что мне нравится, как он был написан. Было бы неплохо получить простую версию этого кода, которая у вас здесь, в качестве настраиваемого хука. Кроме того, «await-here» - довольно хороший пакет, который может избавить от необходимости в блоке try / catch.
Девин Дж. Род,
1

пытаться

const MyFunctionnalComponent: React.FC = props => {
  useEffect(() => {
    // Using an IIFE
    (async function anyNameFunction() {
      await loadContent();
    })();
  }, []);
  return <div></div>;
};

Нишла
источник