user11559261
user11559261

Reputation:

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

I have this code

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);

I created three links that open the Miliko component. but when I quickly click on the links I get this error:

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

Upvotes: 87

Views: 127071

Answers (11)

windmaomao
windmaomao

Reputation: 7671

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 it.

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

Demo

https://codepen.io/windmaomao/pen/jOLaOxO , fetch with useAsync https://codepen.io/windmaomao/pen/GRvOgoa , manual fetch with useAsync

Update

The above answer leads to the following component that we use inside our team.

/**
 * 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 } = useAsync({
 *    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
  }
}

Update 2022

This approach has been adopted in the book https://www.amazon.com/Designing-React-Hooks-Right-Way/dp/1803235950 where this topic has been mentioned in useRef and custom hooks chapters, and more examples are provided there.

Update 2023

Google AI response: React 18 no longer shows a warning about memory leaks when you try to update the state of a component that has been removed/unmounted. This is because React 18 has improved its memory management so that it is less likely to cause memory leaks. However, there are still some cases where it is possible to cause a memory leak in React 18. One way to do this is to create an event listener that is not removed when the component unmounts. Another way to cause a memory leak is to use a ref that is not cleaned up when the component unmounts. If you are experiencing memory leaks in your React 18 application, you can use the React DevTools to track down the source of the leak. The React DevTools will show you which components are using the most memory and which components are not being unmounted properly. Once you have identified the source of the leak, you can fix it by removing the event listener or cleaning up the ref.

I created a pen to demo it but failed: https://codepen.io/windmaomao/pen/XWyLrOa?editors=1011

Upvotes: 110

arman amirkamali
arman amirkamali

Reputation: 111

For this problem I used a tricky way

first I deploy a state like this

const [routing,setRouting] = useState(false)

then when my works finished I changed it to true and change my useEffect like this

useEffect(()=>{
if(routing)
    navigation.navigate('AnotherPage')

),[routing]}

Upvotes: 0

rancharaz90
rancharaz90

Reputation: 11

const [getAllJobs, setgetAlljobs] = useState();

useEffect(() => {
    let mounted = true;
    axios.get('apiUrl')
        .then(function (response) {
            const jobData = response.data;
            if (mounted) {
                setgetAlljobs(jobData)
            }
        })
        .catch(function (error) {
            console.log(error.message)
        })
    return () => mounted = false;

}, [])

set a variable mounted to true-> then if it is true, mount the function-> in the bottom you return it to unmount it

Upvotes: 1

Elmar  Amanov
Elmar Amanov

Reputation: 31

import React, { useCallback, useEffect, useRef, useState } from "react";
import { userLoginSuccessAction } from "../../../redux/user-redux/actionCreator";
import { IUser } from "../../../models/user";
import { Navigate } from "react-router";
import XTextField from "../../../x-lib/x-components/x-form-controls/XTextField";
import { useDispatch } from "react-redux";
interface Props {
  onViewChange?: (n: number) => void;
  userInit?: (user: IUser) => void;
}

interface State {
  email: string;
  password: string;
  hasError?: boolean;
  errorMessage?: string;
}

const initialValue = {
  email: "[email protected]",
  password: "cityslicka",
  errorMessage: "",
};
const LoginView: React.FC<Props> = (props) => {
  const { onViewChange } = props;
  const [state, setState] = useState(initialValue);
  const mountedRef = useRef(true);
  const dispatch = useDispatch();
  const handleEmailChange = useCallback(
    (val: string) => {
      setState((state) => ({
        ...state,
        email: val,
      }));
    },
    [state.email]
  );

  const handlePasswordChange = useCallback(
    (val: string) => {
      setState((state) => ({
        ...state,
        password: val,
      }));
    },
    [state.password]
  );

  const  onUserClick = useCallback( async () =>  {
    // HTTP Call
    const data = {email: state.email , password: state.password}
    try{
      await dispatch(userLoginSuccessAction(data));
      <Navigate to = '/' />
      setState( (state)=>({
        ...state,
        email: "",
        password: ""
      })) 
    }
    catch(err){
      setState( (state)=>({
        ...state,
        errorMessage: err as string
      }))
    }
  },[mountedRef] )
  
  useEffect(()=>{
    onUserClick();
    return ()=> {
      mountedRef.current = false;
    };
  },[onUserClick]); 
  
  const Error = (): JSX.Element => {
    return (
      <div
        className="alert alert-danger"
        role="alert"
        style={{ width: "516px", margin: "20px auto 0 auto" }}
      >
        {state.errorMessage}
      </div>
    );
  };

  return (
    <div>
      <div>
        email: "[email protected]"
        <span style={{ paddingRight: "20px" }}></span> password: "cityslicka"{" "}
      </div>
      {state.errorMessage && <Error />}
      <form className="form-inline">
        <div className="form-group">
          <XTextField
            label="email"
            placeholder="E-Posta"
            value={state.email}
            onChange={handleEmailChange}
          />
        </div>
        <div className="form-group my-sm-3">
          <XTextField
            type="password"
            label="password"
            placeholder="Şifre"
            value={state.password}
            onChange={handlePasswordChange}
          />
        </div>
        <button type="button" className="btn btn-primary" onClick = {onUserClick} >
          Giriş Et
        </button>
        <a
          href="#"
          onClick={(e) => {
            e.preventDefault();
            onViewChange && onViewChange(3);
          }}
        >
          Şifremi Unuttum!
        </a>
      </form>

      <p>
        Hələdə üye deyilsiniz? <br />
        pulsuz registir olmak üçün
        <b>
          <u>
            <a
              style={{ fontSize: "18px" }}
              href="#"
              onClick={(e) => {
                e.preventDefault();
                onViewChange && onViewChange(2);
              }}
            >
              kilik edin.
            </a>
          </u>
        </b>
      </p>
    </div>
  );
};

export default LoginView;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

Upvotes: 0

Mohammed Alshaer
Mohammed Alshaer

Reputation: 503

you can wrap any action as a callback inside checkUnmount

const useUnmounted = () => {
  const mountedRef = useRef(true);

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

  const checkUnmount = useCallback(
    (cb = () => {}) => {
      try {
        if (!mountedRef.current) throw new Error('Component is unmounted');
        cb();
      } catch (error) {
        console.log({ error });
      }
    },
    [mountedRef.current],
  );

  return [checkUnmount, mountedRef.current];
};

Upvotes: 0

cmcodes
cmcodes

Reputation: 1866

Create a mutable ref object and set it to true, and during clean-up toggle its value, to ensure that the component has been unmouted.

const mountedRef = useRef(true)

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

Upvotes: 1

MorenoMdz
MorenoMdz

Reputation: 336

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

Upvotes: 2

Martin Melichar
Martin Melichar

Reputation: 1156

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.

Upvotes: 0

Agent
Agent

Reputation: 1395

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.

Upvotes: 17

Favour George
Favour George

Reputation: 1933

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

Upvotes: 46

Easwar
Easwar

Reputation: 5402

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.

Upvotes: 7

Related Questions