React Hook在useEffect中使用async函数的警告:useEffect函数必须返回一个清理函数或什么也不返回

Red*_*daz 59 javascript reactjs react-hooks

我正在尝试下面的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);
    }
}, []);
Run Code Online (Sandbox Code Playgroud)

我在我的控制台中收到此警告.但我认为清理对于异步调用是可选的.我不知道为什么我会收到这个警告.链接沙箱的例子.https://codesandbox.io/s/24rj871r0p 在此输入图像描述

RTW*_*RTW 72

我建议看看Dan Abramov(其中一位反应创造者)回答:

我认为你使它变得比它需要的更复杂.

export default function Example() { 
    const [data, dataSet] = useState(false)

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

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

  return <div>{data}</div>
}
Run Code Online (Sandbox Code Playgroud)

从长远来看,我们会劝阻这种模式,因为它会鼓励竞争条件.比如 - 你的通话开始和结束之间可能发生任何事情,你可能会得到新的道具.相反,我们建议使用Suspense进行数据提取,这看起来更像

const response = MyAPIResource.read();
Run Code Online (Sandbox Code Playgroud)

并没有影响.但在此期间,您可以将异步内容移动到单独的函数并调用它.

  • 您还可以使用名为 [use-async-effect](https://www.npmjs.com/package/use-async-effect) 的包。此包使您能够使用 async await 语法。 (6认同)
  • 嘿,这很好,也是我大多数时候所做的。但是 `eslint` 要求我将 `fetchMyAPI()` 作为 `useEffect` 的依赖项 (4认同)
  • 您可以通过检查组件是否已卸载来解决竞争条件问题:`useEffect(() =&gt; { let unmounted = false promise.then(res =&gt; { if (!unmounted) { setState(...) } }) return () =&gt; { unmounted = true } }, [])` (2认同)

Nid*_*eph 40

您还可以使用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>
}
Run Code Online (Sandbox Code Playgroud)


Shu*_*tri 36

当你使用async函数时

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

它返回一个promise并且useEffect不希望回调函数返回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);
        }
    })();
}, []);
Run Code Online (Sandbox Code Playgroud)

工作代码盒

  • 但 eslint 不会容忍 (2认同)
  • 您是在说我可以在异步功能中加入清理功能吗?我试过了,但是我的清理功能从未被调用过。你能举个例子吗? (2认同)

Ed *_*d I 27

在React提供更好的方法之前,您可以创建一个帮助程序useEffectAsync.js:

import { useEffect } from 'react';


export default function useEffectAsync(effect, inputs) {
    useEffect(() => {
        effect();
    }, inputs);
}
Run Code Online (Sandbox Code Playgroud)

现在您可以传递异步函数:

useEffectAsync(async () => {
    const items = await fetchSomeItems();
    console.log(items);
}, []);
Run Code Online (Sandbox Code Playgroud)

  • React 不会自动允许 useEffect 中的异步函数的原因是,在很大一部分情况下,需要进行一些清理。您编写的函数“useAsyncEffect”很容易误导人们,让他们认为如果他们从异步效果中返回一个清理函数,那么它将在适当的时间运行。这可能会导致内存泄漏或更严重的错误,因此我们选择鼓励人们重构他们的代码,以使与 React 生命周期交互的异步函数的“接缝”更加明显,并且代码的行为有望更加深思熟虑和正确。 (12认同)

kaj*_*kal 16

此处可以使用void 运算符
代替:

React.useEffect(() => {
    async function fetchData() {
    }
    fetchData();
}, []);
Run Code Online (Sandbox Code Playgroud)

或者

React.useEffect(() => {
    (async function fetchData() {
    })()
}, []);
Run Code Online (Sandbox Code Playgroud)

你可以写:

React.useEffect(() => {
    void async function fetchData() {
    }();
}, []);
Run Code Online (Sandbox Code Playgroud)

它更干净,更漂亮。


异步效应可能会导致内存泄漏,因此在组件卸载时执行清理非常重要。在获取的情况下,这可能如下所示:

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>;
}
Run Code Online (Sandbox Code Playgroud)


Chi*_*jib 12

我通读了这个问题,感觉答案中没有提到实现 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]);
Run Code Online (Sandbox Code Playgroud)

参考 => https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects

  • 您不是还需要“then(res =&gt; res.json())”行吗? (2认同)

Sim*_*mon 8

对于其他读者,错误可能来自于没有括号包裹 async 函数的事实:

考虑异步函数 initData

  async function initData() {
  }
Run Code Online (Sandbox Code Playgroud)

此代码将导致您的错误:

  useEffect(() => initData(), []);
Run Code Online (Sandbox Code Playgroud)

但是这个,不会:

  useEffect(() => { initData(); }, []);
Run Code Online (Sandbox Code Playgroud)

(注意 initData() 周围的括号

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

Rod*_*rez 5

API使用从外部获取数据React Hooks,您应该调用一个从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();
}, []);
Run Code Online (Sandbox Code Playgroud)

我强烈建议你不要在useEffectHook 中定义你的查询,因为它会被无限次重新渲染。并且由于您无法进行useEffect异步,因此您可以将其内部的函数设为异步。

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

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

{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>
))}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

  • 为什么在函数定义的同一块中使用“.then”和“await”?我认为 `await` 的全部意义就是取代 `.then`。 (3认同)

sli*_*wp2 5

其他答案已经通过很多例子给出了,并且解释得很清楚,所以我将从TypeScript类型定义的角度进行解释。

钩子useEffectTypeScript 签名:

function useEffect(effect: EffectCallback, deps?: DependencyList): void;
Run Code Online (Sandbox Code Playgroud)

类型effect

// 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 };
Run Code Online (Sandbox Code Playgroud)

现在我们应该知道为什么effect不能是一个async函数了。

function useEffect(effect: EffectCallback, deps?: DependencyList): void;
Run Code Online (Sandbox Code Playgroud)

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);
};
Run Code Online (Sandbox Code Playgroud)

这个钩子是通过将基本的 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;
Run Code Online (Sandbox Code Playgroud)

描述:

考虑这个例子:

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;
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述 在上面的示例中,当计数器更新时,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;
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述 我希望这个自定义挂钩有助于管理 React 组件中的异步任务。