React useEffect导致:无法在未安装的组件上执行React状态更新

时间:2019-03-02 01:25:19

标签: javascript reactjs fetch react-hooks

获取数据时,我得到:无法在已卸载的组件上执行React状态更新。该应用程序仍然可以运行,但是反应表明我可能导致内存泄漏。

“这是空操作,但是它表明您的应用程序内存泄漏。要修复,请取消使用useEffect清理功能中的所有订阅和异步任务。”

为什么我会不断收到此警告?

我尝试研究以下解决方案:

https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal

https://developer.mozilla.org/en-US/docs/Web/API/AbortController

但这仍然给了我警告。

const  ArtistProfile = props => {
  const [artistData, setArtistData] = useState(null)
  const token = props.spotifyAPI.user_token

  const fetchData = () => {
    const id = window.location.pathname.split("/").pop()
    console.log(id)
    props.spotifyAPI.getArtistProfile(id, ["album"], "US", 10)
    .then(data => {setArtistData(data)})
  }
  useEffect(() => {
    fetchData()
    return () => { props.spotifyAPI.cancelRequest() }
  }, [])

  return (
    <ArtistProfileContainer>
      <AlbumContainer>
        {artistData ? artistData.artistAlbums.items.map(album => {
          return (
            <AlbumTag
              image={album.images[0].url}
              name={album.name}
              artists={album.artists}
              key={album.id}
            />
          )
        })
        : null}
      </AlbumContainer>
    </ArtistProfileContainer>
  )
}

编辑:

在我的api文件中,我添加了AbortController(),并使用了signal,以便取消请求。

export function spotifyAPI() {
  const controller = new AbortController()
  const signal = controller.signal

// code ...

  this.getArtist = (id) => {
    return (
      fetch(
        `https://api.spotify.com/v1/artists/${id}`, {
        headers: {"Authorization": "Bearer " + this.user_token}
      }, {signal})
      .then(response => {
        return checkServerStat(response.status, response.json())
      })
    )
  }

  // code ...

  // this is my cancel method
  this.cancelRequest = () => controller.abort()
}

我的spotify.getArtistProfile()看起来像这样

this.getArtistProfile = (id,includeGroups,market,limit,offset) => {
  return Promise.all([
    this.getArtist(id),
    this.getArtistAlbums(id,includeGroups,market,limit,offset),
    this.getArtistTopTracks(id,market)
  ])
  .then(response => {
    return ({
      artist: response[0],
      artistAlbums: response[1],
      artistTopTracks: response[2]
    })
  })
}

但是因为我的信号用于在Promise.all中解决的单个api调用,所以我无法abort()保证,所以我将始终设置状态。

12 个答案:

答案 0 :(得分:7)

例如,您有一些组件执行一些异步操作,然后将结果写入状态并在页面上显示状态内容:

export default function MyComponent() {
    const [loading, setLoading] = useState(false);
    const [someData, setSomeData] = useState({});
    // ...
    useEffect(() => {
        setLoading(true);
        someResponse = await doVeryLongRequest(); // it needs some time
        // When request is finished:
        setSomeData(someResponse.data); // (1) write data to state
        setLoading(false); // (2) write some value to state
    }, []);

    return (
        <div className={loading ? "loading" : ""}>
            {someData}
            <a href="SOME_LOCAL_LINK">Go away from here!</a>
        </div>
    );
}

假设用户在 doVeryLongRequest() 仍然执行时点击了某个链接。 MyComponent 已卸载但请求仍然存在,当它收到响应时,它会尝试在 (1)(2) 行中设置状态并尝试更改 HTML 中的相应节点。我们会从主题中得到一个错误。

我们可以通过检查组件是否仍然安装来修复它。让我们创建一个变量 componentMounted(下面的 (3) 行)并将其设置为 true。卸载组件后,我们会将其设置为 false(下面的行 (4))。每次尝试设置状态时,让我们检查 componentMounted 变量(下面的行 (5))。

修复代码:

export default function MyComponent() {
    const [loading, setLoading] = useState(false);
    const [someData, setSomeData] = useState({});
    let componentMounted = true; // (3) component is mounted
    // ...
    useEffect(() => {
        setLoading(true);
        someResponse = await doVeryLongRequest(); // it needs some time
        // When request is finished:
        if (componentMounted){ // (5) is component still mounted?
            setSomeData(someResponse.data); // (1) write data to state
            setLoading(false); // (2) write some value to state
        }
        return () => { // This code runs when component is unmounted
            componentMounted = false; // (4) set it to false if we leave the page
        }
    }, []);

    return (
        <div className={loading ? "loading" : ""}>
            {someData}
            <a href="SOME_LOCAL_LINK">Go away from here!</a>
        </div>
    );
}

答案 1 :(得分:2)

AbortController个请求之间共享fetch()是正确的方法。
Promise 任何 被中止时,Promise.all()将被AbortError拒绝:

function Component(props) {
  const [fetched, setFetched] = React.useState(false);
  React.useEffect(() => {
    const ac = new AbortController();
    Promise.all([
      fetch('http://placekitten.com/1000/1000', {signal: ac.signal}),
      fetch('http://placekitten.com/2000/2000', {signal: ac.signal})
    ]).then(() => setFetched(true))
      .catch(ex => console.error(ex));
    return () => ac.abort(); // Abort both fetches on unmount
  }, []);
  return fetched;
}
const main = document.querySelector('main');
ReactDOM.render(React.createElement(Component), main);
setTimeout(() => ReactDOM.unmountComponentAtNode(main), 1); // Unmount after 1ms
<script src="//cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.development.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.development.js"></script>
<main></main>

答案 2 :(得分:1)

对我来说,清理组件卸载状态是有帮助的。

 const [state, setState] = useState({});

useEffect(() => {
    myFunction();
    return () => {
      setState({}); // This worked for me
    };
}, []);

const myFunction = () => {
    setState({
        name: 'Jhon',
        surname: 'Doe',
    })
}

答案 3 :(得分:1)

我在滚动到顶部时遇到了类似的问题,@CalosVallejo 的回答解决了它:) 非常感谢!!

const ScrollToTop = () => { 

  const [showScroll, setShowScroll] = useState();

//------------------ solution
  useEffect(() => {
    checkScrollTop();
    return () => {
      setShowScroll({}); // This worked for me
    };
  }, []);
//-----------------  solution

  const checkScrollTop = () => {
    setShowScroll(true);
 
  };

  const scrollTop = () => {
    window.scrollTo({ top: 0, behavior: "smooth" });
 
  };

  window.addEventListener("scroll", checkScrollTop);

  return (
    <React.Fragment>
      <div className="back-to-top">
        <h1
          className="scrollTop"
          onClick={scrollTop}
          style={{ display: showScroll }}
        >
          {" "}
          Back to top <span>&#10230; </span>
        </h1>
      </div>
    </React.Fragment>
  );
};

答案 4 :(得分:0)

您可以尝试将此状态设置为类似状态,并检查是否已安装组件。这样,您可以确定如果卸载了组件,则不会尝试获取某些东西。

const [didMount, setDidMount] = useState(false); 

useEffect(() => {
   setDidMount(true);
   return () => setDidMount(false);
}, [])

if(!didMount) {
  return null;
}

return (
    <ArtistProfileContainer>
      <AlbumContainer>
        {artistData ? artistData.artistAlbums.items.map(album => {
          return (
            <AlbumTag
              image={album.images[0].url}
              name={album.name}
              artists={album.artists}
              key={album.id}
            />
          )
        })
        : null}
      </AlbumContainer>
    </ArtistProfileContainer>
  )

希望这会对您有所帮助。

答案 5 :(得分:0)

当您导航到其他组件后对当前组件执行状态更新时,会发生此错误:

例如

  axios
      .post(API.BASE_URI + API.LOGIN, { email: username, password: password })
      .then((res) => {
        if (res.status === 200) {
          dispatch(login(res.data.data)); // line#5 logging user in
          setSigningIn(false); // line#6 updating some state
        } else {
          setSigningIn(false);
          ToastAndroid.show(
            "Email or Password is not correct!",
            ToastAndroid.LONG
          );
        }
      })

在第 5 行的上述情况下,我正在调度 login 操作,该操作作为回报将用户导航到仪表板,因此登录屏幕现在被卸载。
现在,当 React Native 到达第 6 行并看到状态正在更新时,它会大声喊叫我该怎么做,login component 已经没有了。

解决方案:

  axios
      .post(API.BASE_URI + API.LOGIN, { email: username, password: password })
      .then((res) => {
        if (res.status === 200) {
          setSigningIn(false); // line#6 updating some state -- moved this line up
          dispatch(login(res.data.data)); // line#5 logging user in
        } else {
          setSigningIn(false);
          ToastAndroid.show(
            "Email or Password is not correct!",
            ToastAndroid.LONG
          );
        }
      })

只需移动上面的反应状态更新,将第 6 行移动到第 5 行。
现在,在将用户导航离开之前,状态正在更新。赢赢

答案 6 :(得分:0)

如果用户导航离开,或其他原因导致组件在异步调用返回并尝试对其进行 setState 之前被销毁,则会导致错误。如果它确实是一个延迟完成的异步调用,它通常是无害的。有几种方法可以消除错误。

如果你正在实现一个像 useAsync 这样的钩子,你可以用 let 而不是 const 来声明你的 useStates,并且在 useEffect 返回的析构函数中,设置 setState 函数) 到无操作函数。


export function useAsync<T, F extends IUseAsyncGettor<T>>(gettor: F, ...rest: Parameters<F>): IUseAsync<T> {
  let [parameters, setParameters] = useState(rest);
  if (parameters !== rest && parameters.some((_, i) => parameters[i] !== rest[i]))
    setParameters(rest);

  const refresh: () => void = useCallback(() => {
    const promise: Promise<T | void> = gettor
      .apply(null, parameters)
      .then(value => setTuple([value, { isLoading: false, promise, refresh, error: undefined }]))
      .catch(error => setTuple([undefined, { isLoading: false, promise, refresh, error }]));
    setTuple([undefined, { isLoading: true, promise, refresh, error: undefined }]);
    return promise;
  }, [gettor, parameters]);

  useEffect(() => {
    refresh();
    // and for when async finishes after user navs away //////////
    return () => { setTuple = setParameters = (() => undefined) } 
  }, [refresh]);

  let [tuple, setTuple] = useState<IUseAsync<T>>([undefined, { isLoading: true, refresh, promise: Promise.resolve() }]);
  return tuple;
}

不过,这在组件中效果不佳。在那里,您可以将 useState 包装在跟踪安装/卸载的函数中,并使用 if-check 包装返回的 setState 函数。

export const MyComponent = () => {
  const [numPendingPromises, setNumPendingPromises] = useUnlessUnmounted(useState(0));
  // ..etc.

// imported from elsewhere ////

export function useUnlessUnmounted<T>(useStateTuple: [val: T, setVal: Dispatch<SetStateAction<T>>]): [T, Dispatch<SetStateAction<T>>] {
  const [val, setVal] = useStateTuple;
  const [isMounted, setIsMounted] = useState(true);
  useEffect(() => () => setIsMounted(false), []);
  return [val, newVal => (isMounted ? setVal(newVal) : () => void 0)];
}

然后您可以创建一个 useStateAsync 钩子来简化一些。

export function useStateAsync<T>(initialState: T | (() => T)): [T, Dispatch<SetStateAction<T>>] {
  return useUnlessUnmounted(useState(initialState));
}

答案 7 :(得分:0)

尝试在useEffect中添加依赖:

  useEffect(() => {
    fetchData()
    return () => { props.spotifyAPI.cancelRequest() }
  }, [fetchData, props.spotifyAPI])

答案 8 :(得分:0)

我收到了同样的警告,这个解决方案对我有用 ->

useEffect(() => {
    const unsubscribe = fetchData(); //subscribe
    return unsubscribe; //unsubscribe
}, []);

如果你有多个获取函数,那么

const getData = () => {
    fetch1();
    fetch2();
    fetch3();
}

useEffect(() => {
    const unsubscribe = getData(); //subscribe
    return unsubscribe; //unsubscribe
}, []);

答案 9 :(得分:0)

简单的方法

    let fetchingFunction= async()=>{
      // fetching
    }

React.useEffect(() => {
    fetchingFunction();
    return () => {
        fetchingFunction= null
    }
}, [])

答案 10 :(得分:0)

有很多答案,但我想我可以更简单地演示 abort 是如何工作的(至少它是如何为我解决问题的):

useEffect(() => {
  // get abortion variables
  let abortController = new AbortController();
  let aborted = abortController.signal.aborted; // true || false
  async function fetchResults() {
    let response = await fetch(`[WEBSITE LINK]`);
    let data = await response.json();
    aborted = abortController.signal.aborted; // before 'if' statement check again if aborted
    if (aborted === false) {
      // All your 'set states' inside this kind of 'if' statement
      setState(data);
    }
  }
  fetchResults();
  return () => {
    abortController.abort();
  };
}, [])

其他方法: https://medium.com/wesionary-team/how-to-fix-memory-leak-issue-in-react-js-using-hook-a5ecbf9becf8

答案 11 :(得分:0)

选项={{ 过滤器类型:“复选框” , 文本标签:{ 身体: { noMatch: isLoading ? : '抱歉,没有要显示的匹配数据', }, }, }}