React Hook警告:在useEffect中使用异步函数,useEffect函数必须返回一个清理函数或者什么都不返回。

517

我尝试使用以下类似的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/24rj871r0penter image description here


4
对于那些想知道背后解释的人,这里有一篇很好的文章:https://devtrium.com/posts/async-functions-useeffect问题在于useEffect的第一个参数应该是一个返回无(未定义)或函数(用于清除副作用)的函数。但是异步函数返回一个Promise,它不能被调用为函数!这不是useEffect钩子期望其第一个参数的内容。 - Pavel
18个回答

791

针对React版本<=17

我建议查看React核心维护者之一Dan Abramov在此处的回答:

我认为你正在让它变得比必要的复杂。

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({ 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

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

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

针对 React 版本 >=18

从 React 18 开始,您也可以使用 Suspense,但是如果您没有使用正确实现它的框架,则目前还不建议使用:

在 React 18 中,您可以在像 Relay、Next.js、Hydrogen 或 Remix 这样的有偏见的框架中开始使用 Suspense 进行数据获取。虽然可以通过 Suspense 进行临时数据获取,但仍不建议将其作为一般策略。

如果不是框架的一部分,则可以尝试一些实现它的库,例如 swr


悬停效果的简单示例。您需要抛出一个 Promise 让 Suspense 捕捉它,首先显示“fallback”组件,当 Promise 解决时再渲染“Main”组件。

let fullfilled = false;
let promise;

const fetchData = () => {
  if (!fullfilled) {
    if (!promise) {
      promise = new Promise(async (resolve) => {
        const res = await fetch('api/data')
        const data = await res.json()

        fullfilled = true
        resolve(data)
      });
    }

    throw promise
  }
};

const Main = () => {
  fetchData();
  return <div>Loaded</div>;
};

const App = () => (
  <Suspense fallback={"Loading..."}>
    <Main />
  </Suspense>
);

5
您可以通过检查组件是否已卸载来解决竞态条件问题,实现方法如下:`useEffect(() => { let unmounted = false promise.then(res => { if (!unmounted) { setState(...) } })return () => { unmounted = true } }, [])` - Richard
8
你还可以使用名为use-async-effect的软件包。该软件包允许你使用async await语法。 - KittyCat
使用自调用函数,不要让async泄漏到useEffect函数定义中,或者使用一个触发异步调用的自定义函数作为useEffect的包装器,这是目前最好的选择。虽然你可以像建议使用use-async-effect一样包含一个新的包,但我认为这是一个简单的问题可以解决。 - Thulani Chivandikwa
5
好的,我会尽力进行翻译。根据您提供的内容,需要翻译为:嘿,那很好,这也是我大多数时候所做的。但是,'eslint'让我把fetchMyAPI()作为useEffect的依赖项。 - Prakash Reddy Potlapadu
你好,如果我使用getContext或localStorage从localStorage中获取数据,我该怎么办? 例如: const {authContext} = useContext(AuthContext) const data = JSON.parse(authContext).post我创建了async await fetch函数并在useEffect中运行,但是那个警告仍然出现。我尝试了其他方法,但是那个警告一直存在:( - Because i hate myself
你可以通过那种方式改变未挂载组件的状态。 - Maziar B.

133

当您使用异步函数时,例如

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

它返回一个 Promise,而 useEffect 不希望回调函数返回 Promise,相反它期望没有返回或者是返回一个函数。

为了解决这个警告,您可以使用自执行的 async 函数。

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

第二种解决方案将使阅读更加容易,并帮助您编写代码来取消先前的请求,如果新请求被触发或将最新的请求响应保存在状态中。

工作中的 Codesandbox


一个使这个过程更容易的包已经被制作出来了。你可以在这里找到它。 - KittyCat
3
但是ESLint不会容忍这种情况。 - Muhaimin CS
1
无法执行清理/DidMount回调。 - David Rearte
1
@ShubhamKhatri 当你使用 useEffect 时,你可以返回一个函数来进行清理工作,比如取消事件的订阅。当你使用异步函数时,你不能返回任何东西,因为 useEffect 不会等待结果。 - David Rearte
2
你是说我可以在异步函数中放置一个清理函数吗?我尝试过,但我的清理函数从未被调用。你能给个小例子吗? - David Rearte
显示剩余2条评论

53

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

更新

如果你选择这种方法,请注意这是一种不好的形式。我在知道是安全的情况下会采用这种方法,但这总是一种不好的形式且往往没有计划。

数据获取的暂停(仍然处于实验阶段)将解决其中的一些情况。

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

或者,您可以将异步结果建模为一个Observable,以便可以基于组件的生命周期进行订阅和取消订阅。


16
React没有自动允许在 useEffect 中使用异步函数的原因是,在很多情况下都需要进行一些清理工作。你所编写的 useAsyncEffect 函数可能会让某些人误以为,如果他们从异步 effect 中返回一个清理函数,它将在适当的时间运行。这可能会导致内存泄漏或更严重的错误,因此我们选择鼓励人们重构代码,使异步函数与 React 生命周期的“接缝”更加明显,从而使代码的行为更加有意识和正确。 - Sophie Alpert

39

你也可以使用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>
}

25

可以在这里使用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>;
}

1
您很了解JS,AbortController是可取消的Promise提案失败后出现的新潮事物。 - Devin Rhode
顺便提一下,有一个名为"use-abortable-fetch"的包,但我不确定它的编写方式是否令人满意。如果能将您这里的代码简化成自定义钩子,那就太好了。此外,"await-here"是一个非常好的包,可以减轻try/catch块的需求。 - Devin Rhode
我更喜欢更短的 React.useEffect(() => { (async () => () {... })();}, []); - Mugen

18

我阅读了这个问题,并觉得实现 useEffect 的最佳方法并没有在回答中提到。 假设你有一个网络调用,并希望在获得响应后执行某些操作。 为简单起见,我们将网络响应存储在状态变量中。 有人可能想使用 action/reducer 将网络响应更新到存储中。

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


2
你是不是也需要添加这一行:then(res => res.json())呢? - nonopolarity
1
是的,没错,但我认为他为了简单起见省略了那个。 - Anthony Luzquiños

12

对于其他读者,错误可能来自于没有用括号包装异步函数:

考虑异步函数initData

  async function initData() {
  }

这段代码会导致错误:

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

但是这一个不会:

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

(注意initData()周围的括号)


1
太棒了,伙计!我正在使用saga,在调用返回唯一对象的action creator时出现了这个错误。看起来useEffect回调函数不喜欢这种行为。感谢你的答案。 - Gorr1995
7
万一有人想知道为什么这是正确的...没有花括号时,initData() 的返回值会被箭头函数隐式返回。有了花括号,就不会隐式返回任何东西,因此错误就不会发生。 - Marnix.hoh

11

使用 React Hooks 从外部 API 获取数据时,应在 useEffect 钩子内调用一个从 API 获取数据的函数。

示例代码如下:

async function fetchData() {
    const res = await fetch("https://swapi.co/api/planets/4/");
    res
      .json()
      .then(res => setPosts(res))
      .catch(err => setErrors(err));
  }

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

我强烈建议不要在 useEffect Hook 内定义查询,因为它会无限次地重新渲染。而且由于你不能使 useEffect 成为异步函数,你可以使其内部的函数成为异步。

在上面展示的示例中,API 调用位于另一个单独的异步函数中,因此可以确保调用是异步的,并且仅发生一次。此外,useEffect 的依赖项数组([])为空,这意味着它的行为就像 React 类组件中的 componentDidMount,它只在组件挂载时执行一次。

对于加载文本,您可以使用 React 的条件渲染来验证您的文章是否为 null,如果是,则呈现加载文本,否则显示文章。当您完成从 API 获取数据并且文章不为 null 时,else 将为 true。

{posts === null ? <p> Loading... </p> 
: posts.map((post) => (
    <Link key={post._id} to={`/blog/${post.slug.current}`}>
      <img src={post.mainImage.asset.url} alt={post.mainImage.alt} />
      <h2>{post.title}</h2>
   </Link>
))}

我看到你已经在使用条件渲染,所以我建议你深入了解它,特别是用于验证对象是否为空!

如果你需要更多有关使用Hooks调用API的信息,我建议你阅读以下文章。

https://betterprogramming.pub/how-to-fetch-data-from-an-api-with-react-hooks-9e7202b8afcd

https://reactjs.org/docs/conditional-rendering.html


3
在函数定义中为什么要同时使用 await.then?我认为 await 的整个意义就是替换 .then - gene b.
我认为关于异步效果的一个重要注意事项是,您应该处理这样的情况:在效果运行但回调执行之前,组件卸载。假设上面的fetch需要500毫秒,并且组件在250毫秒后卸载,则回调将尝试在未安装的组件上更新状态,从而引发错误。 - Magnus Bull
无论函数是异步的还是同步的,警告都会出现。 - undefined

5

已经有许多答案给出了很多例子并且解释清晰,所以我会从TypeScript类型定义的角度来解释它们。

useEffect钩子的TypeScript签名:

function useEffect(effect: EffectCallback, deps?: DependencyList): void;
< p > 效果 的类型:

// NOTE: callbacks are _only_ allowed to return either void, or a destructor.
type EffectCallback = () => (void | Destructor);

// Destructors are only allowed to return void.
type Destructor = () => void | { [UNDEFINED_VOID_ONLY]: never };

现在我们应该知道为什么effect不能是一个async函数。
useEffect(async () => {
  //...
}, [])

异步函数将返回一个带有隐式 undefined 值的 JS Promise。这不符合 useEffect 的预期。

5
你可以使用这个自定义钩子:
import { useEffect, useRef } from "react";
import lodash from "lodash";

export const useEffectAsync = (
  func: () => Promise<any>,
  dependencies: any[]
) => {
  let tasks = useRef<{ func: typeof func }[]>([]);
  const runWaitingTasks = () => {
    if (tasks.current.length) {
      tasks.current[0].func().then(() => {
        let tasksCopy = lodash.cloneDeep(tasks.current);
        tasksCopy.splice(0, 1);
        tasks.current = tasksCopy;
        runWaitingTasks();
      });
    }
  };
  useEffect(() => {
    tasks.current.push({ func });
    if (tasks.current.length === 1) {
      runWaitingTasks();
    }
  }, dependencies);
};

这个钩子是通过将基本的useEffect与队列结合起来,以异步方式管理依赖项的变化而创建的。
简单示例:
import { useState } from "react";
import { useEffectAsync ,anApiCallAsync} from "./Utils";

function App() {
  useEffectAsync(async () => {
    let response = await anApiCallAsync();
    console.log(response)
  }, []);
  return (
    <div>
      <h1>useEffectAsync!</h1>
    </div>
  );
}

export default App;

描述:

考虑以下例子:

import {  useState } from "react";
import { useEffectAsync } from "./Utils";

function App() {
  const [counter, setCounter] = useState(0);
  const sleep = (sleep: number) =>
    new Promise<void>((resolve, reject) => {
      setTimeout(() => {
        resolve();
      }, sleep);
    });

  useEffectAsync(async () => {
    await sleep(1500);
    console.log("useEffectAsync task with delay, counter: " + counter);
  }, [counter]);

  return (
    <div>
      <button onClick={() => setCounter(counter + 1)}>click to increase</button>
      <div>{counter}</div>
    </div>
  );
}

export default App;

输入图像描述 在上面的示例中,当计数器更新时,useEffectAsync钩子确保在执行具有新计数器值的内部函数之前,先完成先前的任务。 实际上,如果任务正在运行并且依赖项同时更改,这个自定义钩子会有效地为传入的任务创建一个队列,并按顺序依次执行,使用新的依赖项值。

这对于您有耗时任务并且需要确保它们按顺序完成并具有更新的依赖项的情况特别有用。

如果您通过useEffect和在其中使用异步函数运行上述示例,每当依赖项更改时, useEffect会立即运行内部函数,而不等待先前的任务完成,可能会导致并发问题。

下面是使用useEffect的示例代码:

import { useEffect, useState } from "react";

function App() {
  const [counter, setCounter] = useState(0);
  const sleep = (sleep: number) =>
    new Promise<void>((resolve, reject) => {
      setTimeout(() => {
        resolve();
      }, sleep);
    });

  useEffect(() => {
    (async () => {
      await sleep(1500);
      console.log("useEffect task with delay, counter: " + counter);
    })();
  }, [counter]);
  return (
    <div>
      <button onClick={() => setCounter(counter + 1)}>click to increase</button>
      <div>{counter}</div>
    </div>
  );
}

export default App;

在这里输入图像描述 希望这个自定义钩子对于在你的React组件中管理异步任务有所帮助。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接