简体   繁体   English

useEffect 中异步 function 的 React Hook 警告:useEffect function 必须返回清理 function 或什么都不返回

[英]React Hook Warnings for async function in useEffect: useEffect function must return a cleanup function or nothing

I was trying the useEffect example something like below:我正在尝试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);
    }
}, []);

and I get this warning in my console.我在我的控制台中收到此警告。 But the cleanup is optional for async calls I think.但我认为清理对于异步调用是可选的。 I am not sure why I get this warning.我不确定为什么会收到此警告。 Linking sandbox for examples.链接沙箱的例子。 https://codesandbox.io/s/24rj871r0p https://codesandbox.io/s/24rj871r0p 在此处输入图像描述

I suggest to look at Dan Abramov (one of the React core maintainers) answer here :我建议在这里查看 Dan Abramov(React 核心维护者之一)的答案

I think you're making it more complicated than it needs to be.我认为你让它变得比它需要的更复杂。

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

Longer term we'll discourage this pattern because it encourages race conditions.从长远来看,我们将不鼓励这种模式,因为它鼓励竞争条件。 Such as — anything could happen between your call starts and ends, and you could have gotten new props.例如 - 在您的通话开始和结束之间可能发生任何事情,并且您可以获得新的道具。 Instead, we'll recommend Suspense for data fetching which will look more like相反,我们会推荐 Suspense 用于数据获取,它看起来更像

const response = MyAPIResource.read();

and no effects.并且没有效果。 But in the meantime you can move the async stuff to a separate function and call it.但与此同时,您可以将异步内容移至单独的函数并调用它。

You can read more about experimental suspense here .您可以在此处阅读有关实验悬念的更多信息。


If you want to use functions outside with eslint.如果你想在 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>
  )
}

If you use useCallback, look at example of how it works useCallback .如果您使用 useCallback,请查看useCallback的工作原理示例。 Sandbox . 沙盒

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

When you use an async function like当您使用异步功能时

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

it returns a promise and useEffect doesn't expect the callback function to return Promise, rather it expects that nothing is returned or a function is returned.它返回一个promise,而useEffect并不期望回调函数返回Promise,而是期望什么都不返回或返回一个函数。

As a workaround for the warning you can use a self invoking async function.作为警告的解决方法,您可以使用自调用异步函数。

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

or to make it more cleaner you could define a function and then call it或者为了让它更干净,你可以定义一个函数然后调用它

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

the second solution will make it easier to read and will help you write code to cancel previous requests if a new one is fired or save the latest request response in state第二种解决方案将使其更易于阅读,并将帮助您编写代码以在触发新请求时取消先前的请求或将最新的请求响应保存在状态

Working codesandbox工作代码框

Until React provides a better way, you can create a helper, useEffectAsync.js :在 React 提供更好的方法之前,您可以创建一个帮助useEffectAsync.js

import { useEffect } from 'react';


export default function useEffectAsync(effect, inputs) {
    useEffect(() => {
        effect();
    }, inputs);
}

Now you can pass an async function:现在你可以传递一个异步函数:

useEffectAsync(async () => {
    const items = await fetchSomeItems();
    console.log(items);
}, []);

Update更新

If you choose this approach, note that it's bad form.如果您选择这种方法,请注意它是错误的形式。 I resort to this when I know it's safe, but it's always bad form and haphazard.当我知道它是安全的时,我会求助于它,但它总是糟糕的形式和随意的。

Suspense for Data Fetching , which is still experimental, will solve some of the cases. Suspense for Data Fetching仍处于试验阶段,将解决一些情况。

In other cases, you can model the async results as events so that you can add or remove a listener based on the component life cycle.在其他情况下,您可以将异步结果建模为事件,以便您可以根据组件生命周期添加或删除侦听器。

Or you can model the async results as an Observable so that you can subscribe and unsubscribe based on the component life cycle.或者您可以将异步结果建模为Observable ,以便您可以根据组件生命周期订阅和取消订阅。

You can also use IIFE format as well to keep things short你也可以使用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>
}

void operator could be used here.此处可以使用void 运算符
Instead of:代替:

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

or或者

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

you could write:你可以写:

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

It is a little bit cleaner and prettier.它更干净更漂亮。


Async effects could cause memory leaks so it is important to perform cleanup on component unmount.异步效果可能会导致内存泄漏,因此在卸载组件时执行清理非常重要。 In case of fetch this could look like this:在 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>;
}

I read through this question, and feel the best way to implement useEffect is not mentioned in the answers.我通读了这个问题,感觉答案中没有提到实现 useEffect 的最佳方法。 Let's say you have a network call, and would like to do something once you have the response.假设您有一个网络呼叫,并且想要在您得到响应后做某事。 For the sake of simplicity, let's store the network response in a state variable.为了简单起见,让我们将网络响应存储在状态变量中。 One might want to use action/reducer to update the store with the network response.有人可能想使用 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]);

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

For other readers, the error can come from the fact that there is no brackets wrapping the async function:对于其他读者,错误可能来自没有括号包裹异步函数的事实:

Considering the async function initData考虑异步函数 initData

  async function initData() {
  }

This code will lead to your error:此代码将导致您的错误:

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

But this one, won't:但是这个,不会:

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

(Notice the brackets around initData() (注意 initData() 周围的括号

For fetching from an external API using React Hooks , you should call a function that fetches from the API inside of the useEffect hook.要使用React Hooks从外部API获取,您应该调用一个从useEffect钩子内部的 API 获取的函数。

Like this:像这样:

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

I strongly recommend that you do not define your query inside the useEffect Hook, because it will be re-render infinite times.我强烈建议您不要在useEffect Hook 中定义您的查询,因为它将无限次重新渲染。 And since you cannot make the useEffect async, you can make the function inside of it to be async.而且由于您无法使useEffect异步,因此您可以将其中的函数设为异步。

In the example shown above, the API call is in another separated async function so it makes sure that the call is async and that it only happens once.在上面显示的示例中,API 调用在另一个单独的异步函数中,因此它确保调用是异步的并且只发生一次。 Also, the useEffect's dependency array (the []) is empty, which means that it will behave just like the componentDidMount from React Class Components, it will only be executed once when the component is mounted.另外, useEffect's依赖数组([])是空的,这意味着它的行为就像 React 类组件中的 componentDidMount,它只会在组件挂载时执行一次。

For the loading text, you can use React's conditional rendering to validate if your posts are null, if they are, render a loading text, else, show the posts.对于加载文本,您可以使用 React 的条件渲染来验证您的帖子是否为空,如果是,则渲染加载文本,否则,显示帖子。 The else will be true when you finish fetching data from the API and the posts are not null.当您完成从 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>
))}

I see you already are using conditional rendering so I recommend you dive more into it, especially for validating if an object is null or not!我看到您已经在使用条件渲染,所以我建议您更深入地研究它,尤其是在验证对象是否为空时!

I recommend you read the following articles in case you need more information about consuming an API using Hooks.如果您需要有关使用 Hooks 使用 API 的更多信息,我建议您阅读以下文章。

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

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

try尝试

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

Other answers have been given by many examples and are clearly explained, so I will explain them from the point of view of TypeScript type definition.其他答案已经很多例子给出并且解释清楚了,所以我将从TypeScript类型定义的角度来解释。

The useEffect hook TypeScript signature: useEffect hook TypeScript 签名:

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

The type of effect : 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 };

Now we should know why effect can't be an async function.现在我们应该知道为什么effect不能是async函数了。

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

The async function will return a JS promise with an implicit undefined value. async 函数将返回一个带有隐式undefined值的 JS 承诺。 This is not the expectation of useEffect .这不是useEffect的期望。

I know it is late but just I had the same problem and I wanted to share that I solved it with a function like this!我知道已经很晚了,但我也遇到了同样的问题,我想分享一下我用这样的函数解决了它!

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

Please try this请试试这个

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

With useAsyncEffect hook provided by a custom library , safely execution of async code and making requests inside effects become trivially since it makes your code auto-cancellable (this is just one thing from the feature list).使用自定义提供的 useAsyncEffect 钩子,安全地执行异步代码和在效果内发出请求变得微不足道,因为它使您的代码可以自动取消(这只是功能列表中的一件事)。 Check out the Live Demo with JSON fetching查看使用 JSON 获取的实时演示

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 使用 axios 的相同演示

Just a note about HOW AWESOME the purescript language handles this problem of stale effects with Aff monad只是关于纯脚本语言如何使用Aff monad 处理这个过时效果问题的注释

WITHOUT PURESCRIPT没有纯文字

you have to use AbortController你必须使用 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>;
}

or stale ( from NoahZinsmeister/web3-react example )stale来自 NoahZinsmeister/web3-react 示例

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

  ...

WITH PURESCRIPT带有纯文本

check how useAff kills it's Aff in the cleanup function检查useAff如何在cleanup功能中杀死它的Aff

the Aff is implemented as a state machine (without promises) Aff被实现为状态机(没有承诺)

but what is relevant to us here is that:但这里与我们相关的是:

Ignore the warning, and use the useEffect hook with an async function like this:忽略警告,并使用带有async functionuseEffect钩子,如下所示:

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

To do it properly and avoid errors: "Warning: Can't perform a React state update on an unmounted..."要正确执行并避免错误:“警告:无法在未安装的设备上执行 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;
    };
  }, []);

OR External functions and using an object或外部函数并使用对象


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

OR AbortController或中止控制器


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




The most easy way is to use useAsyncEffect from 'use-async-effect' You can find it on NPM.最简单的方法是使用“use-async-effect”中的 useAsyncEffect 您可以在 NPM 上找到它。

const ProtectedRoute = ({ children }) => {

    const [isAuth, setIsAuth] = useState(false);

    useAsyncEffect(async () => {
        try {
            const data = await axios("auth");
            console.log(data);
            setIsAuth(true);
        } catch (error) {
            console.log(error);
        }
    }, []);



    if (!isAuth)
        return <Navigate to="/signin" />

    return children;

}

Wrap it into a useCallback and use it as a dependency of the useEffect hook.将其包装成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]);

Best to use SWR for such case.最好在这种情况下使用SWR

Basic example:基本示例:

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

In this example, the useSWR hook accepts a key string and a fetcher function.在这个例子中, useSWR钩子接受一个键字符串和一个 fetcher 函数。 key is a unique identifier of the data (normally the API URL) and will be passed to fetcher . key 是数据的唯一标识符(通常是 API URL),将传递给fetcher fetcher can be any asynchronous function which returns the data, you can use the native fetch or tools like Axios. fetcher可以是任何返回数据的异步函数,您可以使用本机 fetch 或类似 Axios 的工具。

So fetcher can basically be defined with just:所以 fetcher 基本上可以定义为:

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

The hook returns 2 values: data and error, based on the status of the request.该钩子根据请求的状态返回 2 个值:数据和错误。

So you also get error handling there and many other cool features like Automatic Revalidation .因此,您还可以在那里进行错误处理以及许多其他很酷的功能,例如Automatic Revalidation

Source: React Hooks for Data Fetching资料来源:用于数据获取的 React Hooks

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM