在 useEffect React hook 中清理异步函数

5

我有以下useEffect函数,正在尝试找到在组件卸载时清除它的最佳方法。

我认为最好遵循React docs中的makeCancelable,但是当Promise被取消时,代码仍将执行。

const makeCancelable = (promise) => {
  let hasCanceled_ = false;

  const wrappedPromise = new Promise((resolve, reject) => {
    promise.then(
      val => hasCanceled_ ? reject({isCanceled: true}) : resolve(val),
      error => hasCanceled_ ? reject({isCanceled: true}) : reject(error)
    );
  });

  return {
    promise: wrappedPromise,
    cancel() {
      hasCanceled_ = true;
    },
  };
};

//example useEffect
useEffect(() => {
  const getData = async () => {
    const collectionRef_1 = await firestore.collection(...)
    const collectionRef_2 = await firestore.collection(...)
    if (collectionRef_1.exists) {
      //update local state
      //this still runs!
    }
    if (collectionRef_2.exists) {
      //update local state
      //and do does this!
    }
  }
  const getDataPromise = makeCancelable(new Promise(getData))
  getDataPromise.promise.then(() => setDataLoaded(true))
  return () => getDataPromise.cancel()
}, [dataLoaded, firestore])

我还尝试了const getDataPromise = makeCancelable(getData),但没有成功。代码执行得很好,只是在组件卸载时不能正确清除。
我是否需要取消这两个await函数?

makeCancelable(new Promise(getData)) 我认为应该是 makeCancelable(getData()),注意这里是函数调用。 - Yury Tarabanko
1个回答

6
在你的makeCancelable函数中,你只是在承诺已经完成后(也就是getData已经完全执行)检查了hasCanceled_的值:
const makeCancelable = (promise) => {
  let hasCanceled_ = false;

  const wrappedPromise = new Promise((resolve, reject) => {
    // AFTER PROMISE RESOLVES (see following '.then()'!), check if the 
    // react element has unmount (meaning the cancel function was called). 
    // If so, just reject it
    promise.then(
      val => hasCanceled_ ? reject({isCanceled: true}) : resolve(val),
      error => hasCanceled_ ? reject({isCanceled: true}) : reject(error)
    );
  });

  return {
    promise: wrappedPromise,
    cancel() {
      hasCanceled_ = true;
    },
  };
};

相反,在这种情况下,我建议您选择更简单、更经典的解决方案,并使用一个 isMounted 变量来创建所需的逻辑:

useEffect(() => {
  let isMounted = true
  const getData = async () => {
    const collectionRef_1 = await firestore.collection(...)
    const collectionRef_2 = await firestore.collection(...)
    if (collectionRef_1.exists && isMounted) {
      // this should not run if not mounted
    }
    if (collectionRef_2.exists && isMounted) {
      // this should not run if not mounted
    }
  }
  getData().then(() => setDataLoaded(true))
  return () => {
    isMounted = false
  }
}, [dataLoaded, firestore])

啊,我现在明白你的意思了,是在之后检查它,而不是取消它。我会尝试重新编写我的 useEffect 来使其工作。React 表示进行 isMounted 检查是一种反模式,应该尽量避免这样做。 - Charklewis
也许这些文档只适用于类组件?因为你可以在Relay文档中看到Facebook使用钩子实现了这种“反模式” https://relay.dev/docs/en/experimental/step-by-step#23-fetching-graphql-from-react - Rashomon
我认为你可能是对的。文档中的示例正在使用类! - Charklewis
1
顺便提一下,在 React 文档中发现了相同的模式(命名为 ignore 而不是 isMounted):https://reactjs.org/docs/hooks-faq.html#is-it-safe-to-omit-functions-from-the-list-of-dependencies - Rashomon
太棒了,我已经调整了我的函数以使用这种方法,它运行得很好。我在问题中添加了一个链接,指向React说明这是一种反模式的地方。但正如你所提到的,现在越来越清楚,这只是针对类而言的。 - Charklewis

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