7

Each time when onClick executes I received a warning message about memory leak. How component can be can unsubscribed from the Context.Consumer in my functional component with useEffect hook?

I did not find a way how to unsubscribe from the AppContext. AppContext.unsubsribe() did not work.

import React, {useState, useContext} from 'react';
import {withRouter} from 'react-router-dom';
import axios from 'axios';
import {AppContext} from "../context/AppContext";

const LoginPage = (props) => {

    const [name, setName] = useContext(AppContext);
    const [isLoading, setIsLoading] = useState(false);

    const onClick = () => {
        setIsLoading(true);
        axios.post('/get-name')
            .then(resp => {
                setName(resp);
                setIsLoading(false);
                props.history.push('/');
            })
            .catch(err => console.log(err))
            .finally(() => setIsLoading(false));
    };

    return (
        <div>
            <button onClick={onClick}></button>
        </div>
    );
};

export default withRouter(LoginPage);

Error message in the browser console:

Warning: Can't perform a React state update on an unmounted
component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function. in UserPage (created by Context.Consumer) in Route (created by withRouter(UserPage)) in withRouter(LoginPage) (created by Context.Consumer) in Route (created by UserRoute)

Dennis Vash
  • 50,196
  • 9
  • 100
  • 118
kusha
  • 151
  • 1
  • 4
  • 6
  • Is the issue really with the AppContext? I think the issue is that you're doing `setIsLoading` after the component is unmounted. – Retsam Jun 13 '19 at 15:46
  • How should I execute cleanup for the `setIsLoading` method and `isLoading` property? – kusha Jun 13 '19 at 16:37

3 Answers3

8

Your problem is that axios is returning a promise, so when the component is mounted, it executes axios.post(...) on click. When it THEN unmounts (while the promise could still be "unfinished"), the setState of its finally will execute AFTER the component unmounted.

You can use an easy check if the component is mounted:

import React, {useState, useContext, useEffect} from 'react';
import {withRouter} from 'react-router-dom';
import axios from 'axios';
import {AppContext} from "../context/AppContext";

const LoginPage = (props) => {

    const [name, setName] = useContext(AppContext);
    const [isLoading, setIsLoading] = useState(false);
    const isMounted = useRef(null);

    useEffect(() => {
      // executed when component mounted
      isMounted.current = true;
      return () => {
        // executed when unmount
        isMounted.current = false;
      }
    }, []);

    const onClick = () => {
        setIsLoading(true);
        axios.post('/get-name')
            .then(resp => {
                setName(resp);
                setIsLoading(false);
                props.history.push('/');
            })
            .catch(err => console.log(err))
            .finally(() => {
               if (isMounted.current) {
                 setIsLoading(false)
               }
            });
    };

    return (
        <div>
            <button onClick={onClick}></button>
        </div>
    );
};

export default withRouter(LoginPage);
Bennett Dams
  • 6,463
  • 5
  • 25
  • 45
0

As the warning states, in your UserPage component you need to perform a cleanup on useEffect to avoid memory leaks.

Refer to Docs how to require cleanup after an effect.

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });
Dennis Vash
  • 50,196
  • 9
  • 100
  • 118
  • I have seen this example in the documentation. For me not clear which exactly methods should be executed in my case. I do not have subscription to external component that can provide `unsubscribe` method like `ChatAPI.unsubscribeFromFriendStatus(...)` – kusha Jun 13 '19 at 16:40
  • Add your `UserPage`, even better just make a sandbox https://codesandbox.io/s/new – Dennis Vash Jun 13 '19 at 16:41
0

Thanks to @Bennet Dams I could solve my issue, this is my code following his example

  const isMounted = useRef(null);

  useEffect(() => {
    isMounted.current = true;
    fetchRequestsData();
    return () => {
      isMounted.current = false;
    };
  }, []);

  async function fetchRequestsData() {
  
  //My previous code which led to the warning
    /* const { data } = await axios({
      url: '../api/random/my-requests',
      method: 'get',
    });
    setSuspendedRequests(data.suspended); */

    let data;
    axios
      .get('../api/random/my-requests')
      .then((resp) => {
        data = resp.data;
      })
      .catch((err) => console.log(err))
      .finally(() => {
        if (isMounted.current) {
          setSuspendedRequests(data.suspended);
        }
      });
  }