要修复,请取消使用useEffect清理功能中的所有订阅和异步任务

时间:2019-06-04 20:33:38

标签: reactjs

我有这个代码

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清理功能中的所有订阅和异步任务。

7 个答案:

答案 0 :(得分:27)

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

bool

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 } }, []) 此处用于指示是否仍在安装组件。如果是这样,请继续执行异步调用以更新组件状态,否则,请跳过它们。

这应该是不导致内存泄漏(访问清除的内存)问题的主要原因。

答案 1 :(得分:11)

即使组件已卸载,

useEffect 也会尝试保持与数据获取过程的通信。由于这是一种反模式,会使您的应用程序遭受内存泄漏,因此取消对useEffect的订阅可以优化您的应用程序。

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

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中阅读更多内容

答案 2 :(得分:1)

遵循@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]);

答案 3 :(得分:1)

创建一个可变的 ref 对象并将其设置为 true,并在清理期间切换其值,以确保组件已被取消。

const mountedRef = useRef(true)

useEffect(() => {
  // CALL YOUR API OR ASYNC FUNCTION HERE
  return () => { mountedRef.current = false }
}, [])

答案 4 :(得分:0)

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

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

我建议您阅读官方的docs,在其中清楚地解释了它以及一些可配置的参数。

答案 5 :(得分:0)

如果没有@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来获取数据。

我不能仅仅因为不能在函数内部调用钩子而在内部调用函数。

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

谢谢大家的贡献,帮助我学习了更多有关钩子的用法。

答案 6 :(得分:0)

我的案子与这个问题想要的完全不同。还是我有同样的错误。

我的情况是因为我有一个“列表”,该列表是通过使用数组中的.map呈现的。我需要使用.shift。 (以删除数组中的第一项)

如果数组只有一个项目,那是可以的,但是因为它有2个->第一个被“删除/移位”并且因为我使用了key={index}(而索引来自{{1} }),它假定第二个项目(后来是第一个项目)与转移的项目相同。

React保留了第一项的信息(它们是所有节点),因此,如果第二个节点使用.map,React引发错误,则该组件已经被卸除,因为前一个节点的索引为0,且键为key 0具有与第二个组件相同的键0。

第二个组件正确使用了useEffect(),但是React假设它是由那个先前的节点调用的,该节点不再在现场->导致错误。

我通过添加不同的useEffect属性值(不是索引),但是添加了一些唯一的字符串来解决此问题。