useEffect 中异步函数的 React Hook 警告:useEffect 函数必须返回清理函数或不返回任何内容

IT技术 javascript reactjs react-hooks
2021-02-02 16:13:50

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

6个回答

我建议在此处查看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>
}

从长远来看,我们将不鼓励这种模式,因为它鼓励竞争条件。比如——在你的通话开始和结束之间可能会发生任何事情,你可能会得到新的props。相反,我们会推荐 Suspense 进行数据获取,它看起来更像

const response = MyAPIResource.read();

并且没有效果。但与此同时,您可以将异步内容移动到单独的函数并调用它。

您可以在此处阅读有关实验悬念的更多信息


如果你想在 eslint 之外使用函数。

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

  const fetchMyAPI = useCallback(async () => {
    let response = await fetch('api/data/' + userId)
    response = await response.json()
    dataSet(response)
  }, [userId]) // if userId changes, useEffect will run again
               // if you want to run only once, just leave array empty []

  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>
  );
}
嘿,这很好,我大部分时间都在做。eslint要求我fetchMyAPI()依赖useEffect
2021-03-16 16:13:50
您还可以使用名为use-async-effect的包此包使您能够使用 async await 语法。
2021-03-17 16:13:50
使用自调用函数不会让异步泄漏到 useEffect 函数定义或触发异步调用的函数的自定义实现作为 useEffect 的包装器是目前最好的选择。虽然您可以包含一个新包,如建议的 use-async-effect,但我认为这是一个很容易解决的问题。
2021-03-28 16:13:50
您可以通过检查组件是否像这样卸载来解决竞争条件问题: useEffect(() => { let unmounted = false promise.then(res => { if (!unmounted) { setState(...) } }) return () => { unmounted = true } }, [])
2021-04-05 16:13:50

当您使用异步函数时

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

第二种解决方案将使其更易于阅读,并将帮助您编写代码以在触发新请求时取消先前的请求或将最新的请求响应保存在状态

工作代码沙盒

已经制作了一个使这更容易的包。你可以在这里找到它
2021-03-18 16:13:50
但 eslint 不会容忍
2021-03-19 16:13:50
你是说我可以在异步函数中放置一个清理函数?我试过了,但我的清理功能从未被调用过。你能举个小例子吗?
2021-03-27 16:13:50
@ShubhamKhatri 当您使用时,useEffect您可以返回一个函数来进行清理,例如取消订阅事件。当您使用 async 函数时,您无法返回任何内容,因为useEffect不会等待结果
2021-03-31 16:13:50
没有办法执行清理/didmount回调
2021-04-06 16:13:50

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

更新

如果您选择这种方法,请注意它是错误的形式。当我知道它是安全的时,我会求助于这个,但它总是糟糕的形式和随意的。

Suspense for Data Fetching仍处于试验阶段,将解决一些情况。

在其他情况下,您可以将异步结果建模为事件,以便您可以根据组件生命周期添加或删除侦听器。

或者您可以将异步结果建模为Observable,以便您可以根据组件生命周期订阅和取消订阅。

React 不会自动允许 useEffect 中的异步函数的原因是,在很大一部分情况下,需要进行一些清理。useAsyncEffect您编写的函数很容易误导人们认为如果他们从异步效果中返回清理函数,它将在适当的时间运行。这可能会导致内存泄漏或更严重的错误,因此我们选择鼓励人们重构他们的代码,以使与 React 生命周期交互的异步函数的“接缝”更加明显,从而希望代码的行为更加慎重和正确。
2021-04-03 16:13:50

此处可以使用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>;
}
我更喜欢更短的 React.useEffect(() => { (async () => () {... })();}, []);
2021-03-19 16:13:50
先生,您了解您的 JS。AbortController 是可取消的Promise提案失败后可用的新奇特的东西
2021-03-30 16:13:50
顺便说一句,那里有这个“use-abortable-fetch”包,但我不确定我喜欢它的编写方式。获得您在此处拥有的此代码的简单版本作为自定义钩子会很好。此外,“await-here”是一个非常好的包,可以减少对 try/catch 块的需求。
2021-04-06 16:13:50

你也可以使用IIFE格式来保持简短

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

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

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