Cancel all subscriptions in a useEffect cleanup function created by Context.Consumer
Asked Answered
N

3

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)

Northamptonshire answered 13/6, 2019 at 15:41 Comment(2)
Is the issue really with the AppContext? I think the issue is that you're doing setIsLoading after the component is unmounted.Incorruptible
How should I execute cleanup for the setIsLoading method and isLoading property?Northamptonshire
B
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);
Bauhaus answered 13/6, 2019 at 18:52 Comment(0)
T
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);
    };
  });
Tranship answered 13/6, 2019 at 16:3 Comment(2)
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(...)Northamptonshire
Add your UserPage, even better just make a sandbox codesandbox.io/s/newTranship
M
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);
        }
      });
  }
Millimeter answered 24/2, 2022 at 9:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.