【问题标题】:React Hook Warnings for async function in useEffect: useEffect function must return a cleanup function or nothing在 useEffect 中针对异步函数的 React Hook 警告:useEffect 函数必须返回一个清理函数或什么都不返回
【发布时间】:2020-12-22 07:16:03
【问题描述】:

我正在尝试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

【问题讨论】:

    标签: javascript reactjs react-hooks


    【解决方案1】:

    我建议看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
                   // 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,请查看它如何工作的示例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(() =&gt; { let unmounted = false promise.then(res =&gt; { if (!unmounted) { setState(...) } }) return () =&gt; { 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 中运行,但该警告仍然出现。我尝试了其他方法,但警告永远不会发生:(
    【解决方案2】:

    当你使用异步函数时

    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不会等待结果
    • 你是说我可以在异步中加入一个清理功能吗?我试过了,但我的清理功能从未被调用过。你能举个小例子吗?
    【解决方案3】:

    在 React 提供更好的方法之前,你可以创建一个 helper,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 生命周期交互的异步函数的“接缝”更加明显,从而希望代码的行为更加慎重和正确。
    【解决方案4】:

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

    【讨论】:

      【解决方案5】:

      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(() =&gt; { (async () =&gt; () {... })();}, []);
      【解决方案6】:

      我通读了这个问题,感觉答案中没有提到实现 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 =&gt; res.json())这一行吗?
      • 是的,但我认为他为简单起见省略了这一点
      【解决方案7】:

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

      考虑异步函数initData

        async function initData() {
        }
      

      这段代码会导致你的错误:

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

      但是这个,不会:

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

      (注意 initData() 周围的括号

      【讨论】:

      • 太棒了,伙计!我正在使用 saga,当我调用返回唯一对象的动作创建者时出现了该错误。看起来 useEffect 回调函数并没有舔这种行为。感谢您的回答。
      • 以防万一人们想知道为什么这是真的...如果没有花括号,箭头函数会隐式返回 initData() 的返回值。使用花括号,不会隐式返回任何内容,因此不会发生错误。
      【解决方案8】:

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

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

      如果您需要有关使用 Hook 使用 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 后卸载,回调将尝试更新已卸载组件的状态,并抛出错误。
      【解决方案9】:

      其他的答案已经很多例子给出并且解释清楚了,所以我会从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的期望。

      【讨论】:

        【解决方案10】:

        试试

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

        【讨论】:

          【解决方案11】:

          将其包装成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 吗?在发布虚假评论之前做你的研究
          【解决方案12】:

          只是关于纯脚本语言如何使用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)

          但这里与我们相关的是:

          【讨论】:

            【解决方案13】:

            请试试这个

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

            【讨论】:

              【解决方案14】:

              使用自定义 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

              【讨论】:

                【解决方案15】:

                忽略警告,使用 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>);
                }
                

                【讨论】:

                  【解决方案16】:

                  这种情况最好使用SWR

                  基本示例:

                  import useSWR from 'swr'
                  
                  function Profile() {
                    const { data, error } = useSWR('/api/user', fetcher)
                  
                    if (error) return <div>failed to load</div>
                    if (!data) return <div>loading...</div>
                    return <div>hello {data.name}!</div>
                  }
                  

                  在这个例子中,useSWR 钩子接受一个密钥字符串和一个提取函数。 key 是数据的唯一标识符(通常是 API URL),将传递给fetcherfetcher 可以是任何异步函数返回数据,你可以使用原生 fetch 或 Axios 之类的工具。

                  所以 fetcher 基本上可以定义为:

                  import fetch from 'unfetch'
                  const fetcher = url => fetch(url).then(r => r.json())
                  

                  挂钩根据请求的状态返回 2 个值:数据和错误。

                  因此,您还可以获得错误处理和许多其他很酷的功能,例如 Automatic Revalidation

                  来源:React Hooks for Data Fetching

                  【讨论】:

                    猜你喜欢
                    • 2023-02-04
                    • 1970-01-01
                    • 1970-01-01
                    • 2022-12-05
                    • 2022-01-11
                    • 2020-08-08
                    • 1970-01-01
                    相关资源
                    最近更新 更多