Search code examples
reactjsasynchronousaxiosuse-effect

React useEffect unmount protection is not working as expected


I am doing some async data fetching in useEffect with React to handle user state on page refresh. I am receiving the following error which seems common based on google results:

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.

Based on all the documentation around this, this error is based on potentially updating state when the component has already unmounted. The basic fix suggested for this is to check for a variable status tracking whether the component is still mounted before performing the state update.

However, I have implemented the suggested fix and I still receive the error on page refresh. Any additional guidance is greatly appreciated. The abridged code sample is provided below:

const [user, setUser] = React.useState(undefined);
const [isLoggedIn, setIsLoggedIn] = React.useState(false);

React.useEffect(() => {
    let mounted = true;
    
    axios.get('https://api.endpoint/session', { withCredentials: true }).then(session => {
      if(mounted) {
        setIsLoggedIn(true);
        setUser(session.data);
      }
    });

    return () => mounted = false;
  }, [])

Solution

  • That code works fine (Live Demo):

    import React from "react";
    import axios from "axios";
    
    function TestComponent(props) {
      const [user, setUser] = React.useState(undefined);
      const [isLoggedIn, setIsLoggedIn] = React.useState(false);
    
      React.useEffect(() => {
        let mounted = true;
    
        axios.get(props.url, { withCredentials: true }).then((session) => {
          if (mounted) {
            setIsLoggedIn(true);
            setUser(session.data);
          }
        });
    
        return () => (mounted = false);
      }, [props.url]);
      return (
        <div className="component">
          <div className="caption">useEffect demo:</div>
          <div>{JSON.stringify(user)}</div>
        </div>
      );
    }
    

    Btw you can make your life a bit easier by using custom hooks to perform async auto-cancellable effects (the network request will be aborted on unmounting): Live Demo

    import React from "react";
    import { useAsyncEffect } from "use-async-effect2";
    import cpAxios from "cp-axios";
    
    function TestComponent(props) {
      const [cancel, done, result, err] = useAsyncEffect(
        function* () {
          return (yield cpAxios(props.url).data;
        },
        { states: true, deps: [props.url] }
      );
    
      return (
        <div className="component">
          <div className="caption">useAsyncEffect demo:</div>
          <div>
            {done ? (err ? err.toString() : JSON.stringify(result)) : "loading..."}
          </div>
          <button className="btn btn-danger" onClick={cancel} disabled={done}>
            Cancel async effect
          </button>
        </div>
      );
    }