对useEffect中的异步函数的React Hook警告:useEffect函数必须返回清除函数,否则不返回任何内容


116

我正在尝试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 在此处输入图片说明

Answers:


162

我建议在这里看看Dan Abramov(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>
  );
}

您可以通过检查组件是否已卸载来解决竞赛条件问题,如下所示: useEffect(() => { let unmounted = false promise.then(res => { if (!unmounted) { setState(...) } }) return () => { unmounted = true } }, [])
Richard

1
您还可以使用名为use-async-effect的软件包。该软件包使您可以使用async await语法。
KittyCat

使用自调用函数不会让异步泄漏到useEffect函数定义,或者将触发异步调用作为useEffect的包装器的函数的自定义实现是目前最好的选择。虽然您可以包括建议的use-async-effect之类的新程序包,但我认为这是一个很容易解决的问题。
Thulani Chivandikwa

1
嘿,还好,我大多数时候都在做什么。但eslint要求我fetchMyAPI()作为依赖useEffect
Prakash Reddy Potlapadu

51

当您使用诸如

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);
    }
}

它返回一个useEffectPromise,并且不希望回调函数返回Promise,而是期望不返回任何内容或返回一个函数。

作为警告的解决方法,您可以使用自调用异步功能。

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不会容忍
Muhaimin CS

1
没有办法执行清理/ didmount回调
David Rearte '19

1
使用@ShubhamKhatri时,useEffect您可以返回一个函数来进行清理,例如取消订阅事件。当您使用异步函数时,您将无法返回任何内容,因为useEffect将不等待结果
David Rearte '19

2
您是在说我可以在异步函数中添加清理功能吗?我试过了,但我的清理功能从未被调用过。你能举个例子吗?
David Rearte '19

32

在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);
}, []);

8
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


1

尝试

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


1

对于其他读者,该错误可能是由于没有括号包装异步函数的事实引起的:

考虑异步函数initData

  async function initData() {
  }

此代码将导致您的错误:

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

但是,这不会:

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

(注意initData()的方括号


1
太棒了,伙计!我正在使用传奇,当我调用返回唯一对象的动作创建者时,就会出现该错误。看起来useEffect回调函数不会舔这种行为。感谢您的回答。
Gorr1995

2
万一人们想知道为什么这是真的...如果没有花括号,则箭头函数会隐式返回initData()的返回值。使用花括号,不会隐式返回任何内容,因此不会发生错误。
Marnix.hoh

1

可以在这里使用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();
        void 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>;
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.