venndi
venndi

Reputation: 171

useEffect cleanup function? Memory leak error

I want to understand why don't work properly the useEffect hook without the AbortionController.abort function.

I have a nested route in the app.js like:

<Route path='/profile' element={<PrivateRoute />}>
   <Route path='/profile' element={<Profile />} />
</Route>

than the two component: PrivateRoute:

import { Navigate, Outlet } from 'react-router-dom';
import { useAuthStatus } from '../hooks/useAuthStatus';

export default function PrivateRoute() {
  const { loggedIn, checkingStatus } = useAuthStatus();

  if (checkingStatus) return <h1>Loading...</h1>;
  return loggedIn ? <Outlet /> : <Navigate to='/sign-in' />;
}

Profile:

import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { getAuth, updateProfile } from 'firebase/auth';
import { updateDoc, doc } from 'firebase/firestore';
import { db } from '../firebase.config';
import { toast } from 'react-toastify';

export default function Profile() {
  const auth = getAuth();
  const [changeDetails, setChangeDetails] = useState(false);
  const [formData, setFormData] = useState({
    name: auth.currentUser.displayName,
    email: auth.currentUser.email,
  });

  const { name, email } = formData;

  const navigate = useNavigate();

  const onLogout = () => {
    auth.signOut();
    navigate('/');
  };

  const onSubmit = async (e) => {
    try {
      if (auth.currentUser.displayName !== name) {
        //update display name if fb
        await updateProfile(auth.currentUser, {
          displayName: name,
        });
        //update in firestore
        const userRef = doc(db, 'users', auth.currentUser.uid);
        await updateDoc(userRef, {
          name,
        });
      }
    } catch (error) {
      toast.error('Could not update profile details');
    }
  };

  const onChange = (e) => {
    setFormData((prev) => ({
      ...prev,
      [e.target.id]: e.target.value,
    }));
  };
  return (
    <div className='profile'>
      <header className='profileHeader'>
        <p className='pageHeader'>My Profile</p>
        <button type='button' className='logOut' onClick={onLogout}>
          Logout
        </button>
      </header>
      <main>
        <div className='profileDetailsHeader'>
          <p className='profileDetailsText'>Personal Details</p>
          <p
            className='changePersonalDetails'
            onClick={() => {
              setChangeDetails((prev) => !prev);
              changeDetails && onSubmit();
            }}
          >
            {changeDetails ? 'done' : 'change'}
          </p>
        </div>
        <div className='profileCard'>
          <form>
            <input
              type='text'
              id='name'
              className={!changeDetails ? 'profileName' : 'profileNameActive'}
              disabled={!changeDetails}
              value={name}
              onChange={onChange}
            />
            <input
              type='text'
              id='email'
              className={!changeDetails ? 'profileEmail' : 'profileEmailActive'}
              disabled={!changeDetails}
              value={email}
              onChange={onChange}
            />
          </form>
        </div>
      </main>
    </div>
  );
}

and the custom Hook:

import { useEffect, useState } from 'react';
import { getAuth, onAuthStateChanged } from 'firebase/auth';

export function useAuthStatus() {
  const [loggedIn, setLoggedIn] = useState(false);
  const [checkingStatus, setCheckingStatus] = useState(true);

  useEffect(() => {
    const abortCont = new AbortController();
    const auth = getAuth();
    onAuthStateChanged(auth, (user) => {
      if (user) {
        setLoggedIn(true);
      } else {
        setLoggedIn(false);
      }
      setCheckingStatus(false);
    });
    //  return () => abortCont.abort();
  }, [setLoggedIn, setCheckingStatus]);
  return { loggedIn, checkingStatus };
}

Can you explain me why do I get the error: 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.

I found the solution with the AbortController, but still don't understand what is the problem.

The error appears randomly, sometimes when I log in, sometimes when I'm not logged in, and try to go on the profile page. The app works fine, just want to understand what happens under the hood.

If I understand, if I'm not logged in, then it will rendered the 'Sign-in' page, if I'm logged in, then the 'Profile' page will be rendered, also there is a loading page but it's not the case. So, it's simple, if I'm logged in render this page, if not, render the other page. So where is the Problem? Why do I need the AbortController function?

Upvotes: 0

Views: 395

Answers (1)

Phillip
Phillip

Reputation: 6253

onAuthStateChanges will listen forever in your useEffect hook. You need to unsubscribe every time the hook is run otherwise you will have these memory leaks. In your case the change of the users auth state will try to called setLoggedIn even when the component has been unmounted.

Looking at the documentation for onAuthStateChanged (https://firebase.google.com/docs/reference/js/v8/firebase.auth.Auth#onauthstatechanged) it returns a firebase.Unsubscribe.

You'll have to do something like this:

useEffect(() => {
  const auth = getAuth();
  const unsubscribe = onAuthStateChanged(auth, (user) => {
    if (user) {
      setLoggedIn(true);
    } else {
      setLoggedIn(false);
    }
    setCheckingStatus(false);
  });

  return () => unsubscribe();
})

The callback you can optionally return in a useEffect hook is used for cleanup on subsequent calls.

Upvotes: 2

Related Questions