如何在React中的useEffect()内调用异步函数?

92
我希望能调用异步函数并在UseEffect中获取结果。
我在网上找到的fetch api示例直接放在了useEffect函数中。 如果我的URL改变了,我必须修改所有的fetch。
当我尝试时,出现了错误消息。
这是我的代码。

    async function getData(userId) {
        const data = await axios.get(`http://url/api/data/${userId}`)
            .then(promise => {
                return promise.data;
            })
            .catch(e => {
                console.error(e);
            })
            return data;
    }


    function blabla() {
        const [data, setData] = useState(null);

        useEffect(async () => {
            setData(getData(1))
        }, []);

        return (
            <div>
                this is the {data["name"]}
            </div>
        );
    }

index.js:1375 警告:效果函数除了用于清理的函数外,不应返回任何内容。 看起来你写了 useEffect(async () => ...) 或者返回了一个 Promise。相反,应该在 effect 内部编写 async 函数并立即调用它:

useEffect(() => {
  async function fetchData() {
    // You can await here
    const response = await MyAPI.getData(someId);
    // ...
  }
  fetchData();
}, [someId]); // Or [] if effect doesn't need props or state

2
如果一个函数返回一个 Promise,你可以使用 await.then(...),但不能同时使用。 - Omagerio
这个回答可能解决了问题,但实际上并没有解决问题。你实际上是在启动一个可能随时完成的 Promise。如果你的异步任务与组件的生命周期无关,那么没问题。但否则,你将会遇到麻烦和难以调试的渲染错误。我对 React 的经验不足以确定,但我感觉这也会影响 React 的依赖系统。 - Romain Vincent
6个回答

160

在你的 effect 中创建一个异步函数,等待 getData(1) 的结果,然后调用 setData():

useEffect(() => {
  const fetchData = async () => {
     const data = await getData(1);
     setData(data);
  }

  fetchData();
}, []);

8
如果异步函数在 useEffect 钩子之外定义,与它在钩子内部定义有什么不同? - Leland Reardon
9
如果你想在useEffect钩子之外定义异步函数,你需要将它添加到useEffect的依赖列表中,并使用必要的依赖项将其定义包装成useCallback,以防止不必要的调用。欲了解更多信息,请查看React文档此处 - Fraction
如果getData被拒绝,这样不会导致一个未处理的承诺拒绝吗? - Wyck

45

如果您立即调用它,您可能希望将其用作匿名函数:

useEffect(() => {

  (async () => {
     const data = await getData(1);
     setData(data);
  })();

}, []);

1
@Remi 该函数将立即被调用,不会在其他任何地方使用,因此不需要名称或任何预定义。 - hsusanoo
3
如果组件卸载了,取消请求怎么样? - Remi
1
边缘情况。如果您不取消它,那么即使此组件已卸载,React也会设置状态。一些测试库甚至会抱怨。因此,这个例子并不是推荐的。 - Remi
至少在React Native 0.64.3中,这将导致内存泄漏,直到应用程序冻结,异步调用才会完成。 - Juha Untinen
不推荐,老实说。 - Vahid Amiri
显示剩余3条评论

10
最好按照警告提示的方式操作——在effect中调用async函数。
    function blabla() {
        const [data, setData] = useState(null);

        useEffect(() => {
            axios.get(`http://url/api/data/1`)
             .then(result => {
                setData(result.data);
             })
             .catch(console.error)
        }, []);

        return (
            <div>
                this is the {data["name"]}
            </div>
        );
    }

如果您想将API函数保留在组件之外,也可以这样做:
    async function getData(userId) {
        const data = await axios.get(`http://url/api/data/${userId}`)
            .then(promise => {
                return promise.data;
            })
            .catch(e => {
                console.error(e);
            })
            return data;
    }


    function blabla() {
        const [data, setData] = useState(null);

        useEffect(() => {
            (async () => {
                const newData = await getData(1);
                setData(newData);
            })();
        }, []);

        return (
            <div>
                this is the {data["name"]}
            </div>
        );
    }

2
谢谢你的回答。不幸的是,这正是我不想做的。 如果我的URL改变了,我必须在所有我做过fetch的文件中打补丁。 为了可扩展性,我正在尝试做更像我发布的第一段代码的事情。 - Yassine Layachi
1
你可以使用环境变量。 - Monstar

8

由于getData返回一个Promise,你可以直接使用.then。在你的情况下,这比编写一个异步函数并直接调用它要简单得多。

此外,由于axios.get已经返回了一个Promise,你的getData函数不需要标记为async。这是你代码的简化版本:

function getData(userId) {
    return axios.get(`http://url/api/data/${userId}`)
        .then(promise => promise.data)
        .catch(e => {
            console.error(e);
        });
}


function blabla() {
    const [data, setData] = useState(null);

    useEffect(async () => {
        getData(1).then(setData);
    }, []);

    return (
        <div>
            this is the {data["name"]}
        </div>
    );
}

2

在等待解决之前,组件可能会卸载或重新渲染带有不同的someId

const unmountedRef = useRef(false);
useEffect(()=>()=>(unmountedRef.current = true), []);

useEffect(() => {
  const effectStale = false; // Don't forget ; on the line before self-invoking functions
  (async function() {
    // You can await here
    const response = await MyAPI.getData(someId);

    /* Component has been unmounted. Stop to avoid
       "Warning: Can't perform a React state update on an unmounted component." */
    if(unmountedRef.current) return;

    /* Component has re-rendered with different someId value
       Stop to avoid updating state with stale response */
    if(effectStale) return;

    // ... update component state
  })();
  return ()=>(effectStale = true);
}, [someId]);

考虑在组件挂载之前使用Suspense来加载需要的数据。

0

您仍然可以在钩子之外定义异步函数,并在钩子内调用它。

const fetchData = async () => {
   const data = await getData(1);
   setData(data);
}

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

这不被推荐,因为在异步函数内部无法取消setData - itwasmattgregg
2
你说的“cancel”是什么意思? - Yusuf
@itwasmattgregg,你能举个例子来说明取消的操作吗?或者进一步阐述一下吗?我还没有看到任何明确的答案来解释为什么不能这样做,除了最佳实践。然而,最佳实践也可以是将这些函数定义分解到另一个文件中。 - Dev
5
如果在 getData 正在请求数据时组件被卸载,那么 setData 尝试在此之后修改状态,React 会抛出警告,指示可能存在内存泄漏。无论是否真的存在内存泄漏,组件都不应在已经不存在的情况下执行操作。为了避免这种情况,可以从 useEffect 中返回一个函数(React 在卸载时调用它),该函数设置一个标志,然后在调用 setData 之前检查该标志。 - stt

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