To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function

后端 未结 6 893
无人及你
无人及你 2021-02-03 23:18

I have this code

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


        
相关标签:
6条回答
  • 2021-02-03 23:23

    Folowing @Niyongabo solution, the way I ended up that fixed it was:

      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]);
    
    0 讨论(0)
  • 2021-02-03 23:31

    Without @windmaomao answer, I could spend other hours trying to figure out how to cancel the subscription.

    In short, I used two hooks respectively useCallback to memoize function and useEffect to fetch data.

      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
    

    I used useEffect to fetch data.

    I could not call the function inside effect simply because hooks can not be called inside a function.

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

    Thanks, everyone your contribution helped me to learn more about the usage of hooks.

    0 讨论(0)
  • 2021-02-03 23:33

    fetchData is an async function which will return a promise. But you have invoked it without resolving it. If you need to do any cleanup at component unmount, return a function inside the effect that has your cleanup code. Try this :

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

    I would suggest reading the official docs where it is clearly explained along with some more configurable parameters.

    0 讨论(0)
  • 2021-02-03 23:39

    I think the problem is caused by dismount before async call finished.

    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 is used here to indicate if the component is still mounted. And if so, continue the async call to update component state, otherwise, skip them.

    This should be the main reason to not end up with a memory leak (access cleanedup memory) issue.

    0 讨论(0)
  • 2021-02-03 23:40

    useEffect will try to keep communications with your data-fetching procedure even while the component has unmounted. Since this is an anti-pattern and exposes your application to memory leakage, cancelling the subscription to useEffect optimizes your app.

    In the simple implementation example below, you'd use a flag (isSubscribed) to determine when to cancel your subscription. At the end of the effect, you'd make a call to clean up.

    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
    }
    

    You can read up more from this blog juliangaramendy

    0 讨论(0)
  • 2021-02-03 23:43

    My case was pretty different from what this questions wants. Still I got the same error.

    My case was because I had a 'list', which was rendered by using .map from array. And I needed to use .shift. (to remove first item in array)

    If array had just one item, it was ok, but since it had 2 of them -> the first one got 'deleted/shifted' and because I used key={index} (while index was from .map), it assumed, that the second item, which later was first, was the same component as the shifted item..

    React kept info from the first item (they were all nodes) and so, if that second node used useEffect(), React threw error, that the component is already dismounted, because the former node with index 0 and key 0 had the same key 0 as the second component.

    The second component correctly used useEffect, but React assumed, that it is called by that former node, which was no longer on the scene -> resulting in error.

    I fixed this by adding different key prop value (not index), but some unique string.

    0 讨论(0)
提交回复
热议问题