ChatGPT解决这个技术问题 Extra ChatGPT

在 useEffect 中针对异步函数的 React Hook 警告:useEffect 函数必须返回一个清理函数或什么都不返回

我正在尝试 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://i.stack.imgur.com/YFRR5.png

对于那些想知道这背后的解释的人,这里有一篇很好的文章:devtrium.com/posts/async-functions-useeffect 这里的问题是 useEffect 的第一个参数应该是一个不返回任何内容(未定义)或一个函数(清理边)的函数影响)。但是异步函数返回一个 Promise,不能作为函数调用!这根本不是 useEffect 钩子对其第一个参数所期望的。

Z
ZiiMakc

我建议看看Dan Abramov (one of the React core maintainers) answer here

我认为你让它变得比它需要的更复杂。

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

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

您可以阅读有关 experimental suspense here 的更多信息。

如果你想在 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>
  )
}

如果您使用 useCallback,请查看其工作原理示例 useCallbackSandbox

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 } }, [])
您还可以使用名为 use-async-effect 的包。此包使您能够使用异步等待语法。
使用自调用函数不会让异步泄漏到 useEffect 函数定义或触发异步调用的函数的自定义实现作为 useEffect 的包装器是目前最好的选择。虽然您可以包含一个新包,如建议的 use-async-effect 我认为这是一个很容易解决的问题。
嘿,这很好,我大部分时间都在做。但 eslint 要求我将 fetchMyAPI() 作为 useEffect 的依赖项
您好,如果我使用 getContext 或 localStorage 从 localStorage 获取类似的数据,我该怎么办。例如 const {authContext} = useContext(AuthContext) const data = JSON.parse(authContext).post 我创建了 async await fetch 函数并在 useEffect 中运行,但该警告仍然出现。我尝试过其他方法,但警告永远不会发生:(
S
Shubham Khatri

当您使用异步功能时

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,而是希望什么都不返回或返回一个函数。

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

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

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

Working codesandbox


已经制作了一个使这更容易的包。您可以找到它here
但 eslint 不会容忍这种情况
没有办法执行 cleanup/didmount 回调
@ShubhamKhatri 当您使用 useEffect 时,您可以返回一个函数来进行清理,例如取消订阅事件。当您使用异步函数时,您无法返回任何内容,因为useEffect不会等待结果
您是说我可以在异步功能中添加一个清理功能吗?我试过了,但我的清理功能从未被调用过。你能举个小例子吗?
E
Ed I

在 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 生命周期交互的异步函数的“接缝”更加明显,从而希望代码的行为更加慎重和正确。
N
Nidhin Joseph

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

k
kajkal

此处可以使用 void operator
代替:

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

或者

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

你可以写:

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

它更干净更漂亮。

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

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

你知道你的JS,先生。 AbortController 是可取消承诺提案失败后可用的新奇事物
顺便说一句,那里有这个“use-abortable-fetch”包,但我不确定我是否喜欢它的编写方式。将这里的代码作为自定义钩子获得一个简单版本会很好。此外,“await-here”是一个非常不错的包,可以减轻对 try/catch 块的需求。
我更喜欢更短的 React.useEffect(() => { (async () => () {... })();}, []);
C
Chiranjib

我通读了这个问题,感觉答案中没有提到实现 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


是不是你还需要行 then(res => res.json())
是的,是的,但我认为他为简单起见省略了
S
Simon

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

考虑异步函数 initData

  async function initData() {
  }

此代码将导致您的错误:

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

但是这个,不会:

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

(注意 initData() 周围的括号


厉害了,伙计!我正在使用 saga,当我调用返回唯一对象的动作创建者时出现了该错误。看起来 useEffect 回调函数并没有舔这种行为。我很感激你的回答。
以防万一人们想知道为什么这是真的……没有花括号,initData() 的返回值由箭头函数隐式返回。使用花括号,不会隐式返回任何内容,因此不会发生错误。
R
Rodrigo Ramirez

要使用 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'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>
))}

我看到您已经在使用条件渲染,所以我建议您更深入地研究它,尤其是在验证对象是否为空时!

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

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

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


为什么在函数定义的同一块中使用 .thenawait?我认为 await 的全部意义在于替换 .then
我认为关于异步效果需要注意的重要一点是,您应该处理在效果运行之后但在执行回调之前卸载组件的情况。假设上面的 fetch 需要 500ms,并且组件在 250ms 后卸载,回调将尝试更新卸载组件的状态,抛出错误。
N
Nishla

尝试

const MyFunctionnalComponent: React.FC = props => { useEffect(() => { // 使用 IIFE (async function anyNameFunction() { await loadContent(); })(); }, []);返回

; };


s
slideshowp2

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

useEffect 挂钩 TypeScript 签名:

function useEffect(effect: EffectCallback, deps?: DependencyList): void;

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

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

useEffect(async () => {
  //...
}, [])

异步函数将返回一个带有隐式 undefined 值的 JS 承诺。这不是 useEffect 的期望。


L
Lizzeth GarDi

我知道已经很晚了,但是我遇到了同样的问题,我想分享一下我用这样的功能解决了它!

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

n
nisaelk

请试试这个

 useEffect(() => {
        (async () => {
          const products = await api.index()
          setFilteredProducts(products)
          setProducts(products)
        })()
      }, [])

D
Dmitriy Mozgovoy

使用自定义 library 提供的 useAsyncEffect 钩子,安全地执行异步代码和在效果内发出请求变得微不足道,因为它使您的代码可以自动取消(这只是功能列表中的一件事)。查看Live Demo with JSON fetching

import React from "react";
import { useAsyncEffect } from "use-async-effect2";
import cpFetch from "cp-fetch";

/*
 Notice: the related network request will also be aborted
 Checkout your network console
 */

function TestComponent(props) {
  const [cancel, done, result, err] = useAsyncEffect(
    function* () {
      const response = yield cpFetch(props.url).timeout(props.timeout);
      return yield response.json();
    },
    { states: true, deps: [props.url] }
  );

  return (
    <div className="component">
      <div className="caption">useAsyncEffect demo:</div>
      <div>
        {done ? (err ? err.toString() : JSON.stringify(result)) : "loading..."}
      </div>
      <button className="btn btn-warning" onClick={cancel} disabled={done}>
        Cancel async effect
      </button>
    </div>
  );
}

export default TestComponent;

The same demo using axios


s
srghma

只是关于纯脚本语言如何使用 Aff monad 处理这个陈旧效果问题的注释

没有纯文字

你必须使用 AbortController

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

stale (from NoahZinsmeister/web3-react example)

function Balance() {
  const { account, library, chainId } = useWeb3React()

  const [balance, setBalance] = React.useState()
  React.useEffect((): any => {
    if (!!account && !!library) {      
      let stale = false
      
      library 
        .getBalance(account)
        .then((balance: any) => {
          if (!stale) {
            setBalance(balance)
          }
        })
        .catch(() => {
          if (!stale) {
            setBalance(null)
          }
        })

      return () => { // NOTE: will be called every time deps changes
        stale = true
        setBalance(undefined)
      }
    }
  }, [account, library, chainId]) // ensures refresh if referential identity of library doesn't change across chainIds

  ...

带有纯文本

检查如何useAff kills it's Aff in the cleanup function

Aff 实现为 state machine (without promises)

但这里与我们相关的是:

Aff 编码如何停止 Aff - 你可以把你的 AbortController 放在这里

如果错误被抛出到光纤或光纤内部,它将停止运行效果(未测试)和 Affs(它不会从第二个示例运行,因此它不会设置平衡(平衡))


c
cn80

忽略警告,将 useEffect 钩子与 async function 一起使用,如下所示:

import { useEffect, useState } from "react";

function MyComponent({ objId }) {
  const [data, setData] = useState();

  useEffect(() => {
    if (objId === null || objId === undefined) {
      return;
    }

    async function retrieveObjectData() {
      const response = await fetch(`path/to/api/objects/${objId}/`);
      const jsonData = response.json();
      setData(jsonData);
    }
    retrieveObjectData();

  }, [objId]);

  if (objId === null || objId === undefined) {
    return (<span>Object ID needs to be set</span>);
  }

  if (data) {
    return (<span>Object ID is {objId}, data is {data}</span>);
  }

  return (<span>Loading...</span>);
}

C
CodingByJerez

要正确执行并避免错误:“警告:无法在未安装的设备上执行 React 状态更新......”


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

或外部函数并使用对象


useEffect(() => {
  let status = { mounted: true };
  query(status);
  return () => {
    status.mounted = false;
  };
}, []);

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

或中止控制器


 useEffect(() => {
    const abortController = new AbortController();
    (async () => {
      try {
        const response = await fetch(`https://www.reddit.com/r/${subreddit}.json`, { signal: abortController.signal });
        const json = await response.json();
        const newPosts = json.data.children.map(it => it.data);
        setPosts(newPosts);
      } catch (e) {
        if(!abortController.signal.aborted){
           console.error(e);
        }
      }
    })();
    return () => {
      abortController.abort();
    };
  }, []);





T
Tom el Safadi

将其包装成 useCallback 并将其用作 useEffect 挂钩的依赖项。

const getPosts = useCallback(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(async () => {
    getPosts();
}, [getPosts]);

您不会在示例中的 useEffect 中等待 getPosts() ,因此这不会按预期工作。
您不需要等待 getPosts 吗?在发布虚假评论之前做你的研究