修复,取消 useEffect 清理函数中的所有订阅和异步任务

IT技术 reactjs
2021-04-14 23:17:32

我有这个代码

import ReactDOM from "react-dom";
import React, { useState, useEffect } from "react";
import { BrowserRouter as Router, Route, Link } from "react-router-dom";

function ParamsExample() {
  return (
    <Router>
      <div>
        <h2>Accounts</h2>
        <Link to="/">Netflix</Link>
        <Route path="/" component={Miliko} />
      </div>
    </Router>
  );
}

const Miliko = ({ match }) => {
  const [data, setData] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [isError, setIsError] = useState(false);

  useEffect(() => {
    (async function() {
      setIsError(false);
      setIsLoading(true);
      try {
        const Res = await fetch("https://foo0022.firebaseio.com/New.json");
        const ResObj = await Res.json();
        const ResArr = await Object.values(ResObj).flat();
        setData(ResArr);
      } catch (error) {
        setIsError(true);
      }
      setIsLoading(false);
    })();
    console.log(data);
  }, [match]);
  return <div>{`${isLoading}${isError}`}</div>;
};

function App() {
  return (
    <div className="App">
      <ParamsExample />
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

我创建了三个打开Miliko组件的链接但是当我快速点击链接时,我收到了这个错误:

要修复,请取消 useEffect 清理函数中的所有订阅和异步任务。

6个回答

我认为这个问题是由异步调用完成之前的卸载引起的。

const useAsync = () => {
  const [data, setData] = useState(null)
  const mountedRef = useRef(true)

  const execute = useCallback(() => {
    setLoading(true)
    return asyncFunc()
      .then(res => {
        if (!mountedRef.current) return null
        setData(res)
        return res
      })
  }, [])

  useEffect(() => {
    return () => { 
      mountedRef.current = false
    }
  }, [])
}

mountedRef此处用于指示组件是否仍处于安装状态。如果是,则继续异步调用以更新组件状态,否则,跳过它们。

这应该是最终不会出现内存泄漏(访问清理内存)问题的主要原因。

更新

上面的答案导致了我们在团队内部使用的以下组件。

/**
 * A hook to fetch async data.
 * @class useAsync
 * @borrows useAsyncObject
 * @param {object} _                props
 * @param {async} _.asyncFunc         Promise like async function
 * @param {bool} _.immediate=false    Invoke the function immediately
 * @param {object} _.funcParams       Function initial parameters
 * @param {object} _.initialData      Initial data
 * @returns {useAsyncObject}        Async object
 * @example
 *   const { execute, loading, data, error } = useAync({
 *    asyncFunc: async () => { return 'data' },
 *    immediate: false,
 *    funcParams: { data: '1' },
 *    initialData: 'Hello'
 *  })
 */
const useAsync = (props = initialProps) => {
  const {
    asyncFunc, immediate, funcParams, initialData
  } = {
    ...initialProps,
    ...props
  }
  const [loading, setLoading] = useState(immediate)
  const [data, setData] = useState(initialData)
  const [error, setError] = useState(null)
  const mountedRef = useRef(true)

  const execute = useCallback(params => {
    setLoading(true)
    return asyncFunc({ ...funcParams, ...params })
      .then(res => {
        if (!mountedRef.current) return null
        setData(res)
        setError(null)
        setLoading(false)
        return res
      })
      .catch(err => {
        if (!mountedRef.current) return null
        setError(err)
        setLoading(false)
        throw err
      })
  }, [asyncFunc, funcParams])

  useEffect(() => {
    if (immediate) {
      execute(funcParams)
    }
    return () => {
      mountedRef.current = false
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [])

  return {
    execute,
    loading,
    data,
    error
  }
}
谢谢,终于找到了一种在异步任务运行时取消异步任务的方法
2021-05-26 23:17:32
我糊涂了?你能举个例子吗?
2021-06-03 23:17:32

即使组件已卸载,useEffect也会尝试与您的数据获取过程保持通信。由于这是一种反模式并且会将您的应用程序暴露给内存泄漏,因此取消对 useEffect 的订阅可以优化您的应用程序。

在下面的简单实现示例中,您将使用标志(isSubscribed)来确定何时取消订阅。在效果结束时,您会打电话进行清理。

export const useUserData = () => {
  const initialState = {
    user: {},
    error: null
  }
  const [state, setState] = useState(initialState);

  useEffect(() => {
    // clean up controller
    let isSubscribed = true;

    // Try to communicate with sever API
    fetch(SERVER_URI)
      .then(response => response.json())
      .then(data => isSubscribed ? setState(prevState => ({
        ...prevState, user: data
      })) : null)
      .catch(error => {
        if (isSubscribed) {
          setState(prevState => ({
            ...prevState,
            error
          }));
        }
      })

    // cancel subscription to useEffect
    return () => (isSubscribed = false)
  }, []);

  return state
}

你可以从这个博客中阅读更多内容juliangaramendy

如果没有@windmaomao 的回答,我可能会花其他时间来弄清楚如何取消订阅。

简而言之,我分别使用了两个钩子useCallback来记忆函数和useEffect获取数据。

  const fetchSpecificItem = useCallback(async ({ itemId }) => {
    try {
        ... fetch data

      /* 
       Before you setState ensure the component is mounted
       otherwise, return null and don't allow to unmounted component.
      */

      if (!mountedRef.current) return null;

      /*
        if the component is mounted feel free to setState
      */
    } catch (error) {
      ... handle errors
    }
  }, [mountedRef]) // add variable as dependency

我以前是用来useEffect取数据的。

我无法在 effect 内部调用函数,因为无法在函数内部调用钩子。

   useEffect(() => {
    fetchSpecificItem(input);
    return () => {
      mountedRef.current = false;   // clean up function
    };
  }, [input, fetchSpecificItem]);   // add function as dependency

谢谢,大家的贡献帮助我了解了更多关于钩子的用法。

这是帮助我解决警告的唯一评论。谢谢!
2021-05-25 23:17:32
唯一有意义的答案,直接点,在更新状态之前使用占位符检查组件是否仍然安装!
2021-06-16 23:17:32

fetchData是一个异步函数,它将返回一个Promise。但是您在没有解决它的情况下调用了它。如果您需要在组件卸载时进行任何清理,请在具有清理代码的 effect 中返回一个函数。试试这个 :

const Miliko = () => {
  const [data, setData] = useState({ hits: [] });
  const [url, setUrl] = useState('http://hn.algolia.com/api/v1/search?query=redux');
  const [isLoading, setIsLoading] = useState(false);
  const [isError, setIsError] = useState(false);

  useEffect(() => {
    (async function() {
      setIsError(false);
      setIsLoading(true);
      try {
        const result = await axios(url);
        setData(result.data);
      } catch (error) {
        setIsError(true);
      }
      setIsLoading(false);
    })();

    return function() {
      /**
       * Add cleanup code here
       */
    };
  }, [url]);

  return [{ data, isLoading, isError }, setUrl];
};

我建议阅读官方文档,其中清楚地解释了一些更可配置的参数。

我换了鳕鱼。再看一遍。我怎么理解这个问题可以像这个链接一样解决
2021-06-02 23:17:32
是不是同样的错误,因为我只是看了useEffect片段。组件本身的其他部分对我来说看起来很模糊。例如:return [{ data, isLoading, isError }, setUrl];在组件什么理想情况下,渲染应该返回有效的 DOM 或null. 请检查reactjs.org/docs/react-component.html#render
2021-06-17 23:17:32

按照@Niyongabo 解决方案,我最终修复它的方式是:

  const mountedRef = useRef(true);

  const fetchSpecificItem = useCallback(async () => {
    try {
      const ref = await db
        .collection('redeems')
        .where('rewardItem.id', '==', reward.id)
        .get();
      const data = ref.docs.map(doc => ({ id: doc.id, ...doc.data() }));
      if (!mountedRef.current) return null;
      setRedeems(data);
      setIsFetching(false);
    } catch (error) {
      console.log(error);
    }
  }, [mountedRef]);

  useEffect(() => {
    fetchSpecificItem();
    return () => {
      mountedRef.current = false;
    };
  }, [fetchSpecificItem]);
除了不同的提取外,对我来说没有相同的代码:(
2021-05-24 23:17:32