简体   繁体   中英

Why am I getting an error about updating an unmounted component?

In my React app I have configured login using both social media and email and password. The login works, but my redirect to the home page fails. In the console I get the following 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.
LoginPage@http://localhost:3000/static/js/main.chunk.js:3184:101
Route@http://localhost:3000/static/js/vendors~main.chunk.js:109151:29

I have a context called AuthProvder which has the following contents:

import React, { Component, createContext, useContext, useState, useEffect } from "react";
import firebase from "firebase/app";
import { auth, generateUserDocument, projectFirestore, Providers } from "../config/firebase";
import ILocalLoginData from "../interfaces/locallogindata.interface";
import { IProfile } from "../interfaces/profile.interface";

// const UserContext = createContext<Partial<ContextProps>>({});
const AuthContext = createContext({} as any);

const useAuth = () => {
    return useContext(AuthContext);
}

const AuthProvider = ({children}: any) => {

    const [currentUser, setCurrentUser] = useState<any>(null);
    const [loading, setLoading] = useState(true);

    // create the user account in the app database
    // this only needs to be done for the password provider
    // because social media login will create this information
    // for the app
    const signup = async (
        provider: firebase.auth.AuthProvider,
        values?: IProfile
    ) => {

        if (provider.providerId === "password")
        {
            const creds = auth.createUserWithEmailAndPassword(values?.email as string, values?.password as string);

            // add a document to the user collection
            await projectFirestore.collection("users").add({...values});

            return creds;
        }

        return
    }

    const login = (
        provider: firebase.auth.AuthProvider,
        values?: ILocalLoginData
    ) => {
        

        // perform the correct login based on the provider
        if (provider.providerId === "password")
        {
            return auth.signInWithEmailAndPassword(values?.email as string, values?.password as string);
        } else {
            return auth.signInWithPopup(provider);
        }
    }

    const logout = () => {
        return auth.signOut();
    }


    useEffect(() => {
        const unsubscribe = auth.onAuthStateChanged(async user => {

            const usr = await generateUserDocument(user);

            setCurrentUser(usr);
            setLoading(false);

        })

        return unsubscribe;
    }, []);

    const value = {
        currentUser,
        login,
        logout,
        signup
    };

    return (
        <AuthContext.Provider value={value}>
            {!loading && children} 
        </AuthContext.Provider>
    )
}

export { AuthProvider, AuthContext, useAuth };

As can be seen I am returning an unsubscribe function from the useEffect function.

The LoginPage.tsx is as follows:

import React, { useState } from "react";
import { useHistory } from "react-router-dom";
import firebase from "firebase/app";

import IPageProps from "../../interfaces/page.interface";
import { SignIn } from "../../modules/auth";
import { Providers } from "../../config/firebase";
 
import SiteNavbar from "../../comps/Navbar";
import ILocalLoginData from "../../interfaces/locallogindata.interface";

import { useAuth } from "../../contexts/AuthProvider";

const LoginPage: React.FC<IPageProps> = props => {
    const [authenticating, setAuthenticating] = useState<boolean>(false);
    
    const [error, setError] = useState<string>('');
    const [loading, setLoading] = useState<boolean>(false);
    const [values, setValues] = useState<ILocalLoginData>({
        email: "",
        password: ""
    });
    const { login } = useAuth();
    const history = useHistory();  

    const handleLogin = async (e: any, provider: firebase.auth.AuthProvider) => {
        e.preventDefault();

        try {
            setError("");
            setLoading(true);
            await login(provider, values);
            history.push("/");
        } catch {
            setError("Failed to log in");
        }

        setLoading(false);
    }

    // Handle the changes made in the login form so that the values
    // can be extracted
    const handleChange = (e: any) => {
        e.persist();
        setValues(values => ({
            ...values,
            [e.target.name]: e.target.value
        }));
    }

    return (
        <div>
        <SiteNavbar />

        <div className="flex h-screen bg-yellow-700">

            <div className="max-w-xs w-full m-auto bg-yellow-100 rounded p-5">
                <header>
                    <img alt="" className="w-20 mx-auto mb-5" src="https://img.icons8.com/fluent/96/000000/tiger.png" />
                </header>
                <form>
                    <div>
                        <label className="block mb-2 text-yellow-500" htmlFor="email">Email</label>
                        <input className="w-full p-2 mb-6 text-yellow-700 border-b-2 border-yellow-500 outline-none focus:bg-gray-300"
                            type="text"
                            name="email"
                            value={values.email}
                            placeholder="Enter your email address"
                            onChange={handleChange} />
                    </div>
                    <div>
                        <label className="block mb-2 text-yellow-500" htmlFor="password">Password</label>
                        <input className="w-full p-2 mb-6 text-yellow-700 border-b-2 border-yellow-500 outline-none focus:bg-gray-300"
                            type="password"
                            name="password"
                            value={values.password}
                            onChange={handleChange} />
                    </div>
                    <div>
                        <button className="w-full bg-yellow-700 hover:bg-pink-700 text-white font-bold py-2 px-4 mb-6 rounded"
                            onClick={(e) => handleLogin(e, Providers.email)}>
                            Login
                        </button>
                    </div>
                </form>

                <div>
                    <button
                        className="w-full bg-gray-100 hover:bg-pink-700 text-black font-bold py-2 px-4 mb-6 rounded"
                        disabled={authenticating}
                        onClick={(e) => handleLogin(e, Providers.google)}
                    >
                        <i className="fa fa-google"></i>   Login in with Google
                    </button>
                    <button
                        className="w-full bg-indigo-700 hover:bg-pink-700 text-white font-bold py-2 px-4 mb-6 rounded"
                        disabled={authenticating}
                        onClick={(e) => handleLogin(e, Providers.facebook)}
                    >
                        <i className="fa fa-facebook-square"></i>   Login in with Facebook
                    </button>
                </div>
            </div>
        </div>
        </div>
    )
}

export default LoginPage;

And the functions that get all the user details are:

import firebase from 'firebase/app';
import 'firebase/storage';
import 'firebase/firestore';
import 'firebase/auth';
import config from './config';

const Firebase = firebase.initializeApp(config.firebase);

const auth = firebase.auth();
const projectStorage = Firebase.storage();
const projectFirestore = Firebase.firestore();
const timestamp = firebase.firestore.FieldValue.serverTimestamp;

const Providers = {
    google: new firebase.auth.GoogleAuthProvider(),
    email: new firebase.auth.EmailAuthProvider(),
    facebook: new firebase.auth.FacebookAuthProvider(),
}

// Create function that will add any new uses to the user table
const generateUserDocument = async (user: any, additionalData: any = null) => {

    // return if no user has been set
    if (!user) return;

    const userRef = projectFirestore.doc(`users/${user.uid}`);
    const snapshot = await userRef.get();

    // if a snapshot does not exist, e.g. the user does not exist
    // add them to the the collection
    if (!snapshot.exists) {
        const { email, displayName, photoURL } = user;
        try {
            await userRef.set({
                displayName,
                email,
                photoURL,
                enabled: true,
                permitted: false,
                ...additionalData,
            });
        } catch (error) {
            console.error("Error creating user document", error);
        }
    }

    return getUserDocument(user.uid);
};

// get information about the user
const getUserDocument = async (uid: string) => {
    if (!uid) return null;

    try {
        const userDocument = await projectFirestore.doc(`users/${uid}`).get();

        // if the user is not permitted or not enabled return null
        if (userDocument.exists) {
            const doc = userDocument.data();

            if (!doc?.permitted) {
                console.log("not permitted");
                return null;
            } else if (!doc?.enabled) {
                console.log("not enabled");
                return null;
            } else {
                console.log("happy days");
                return {
                    uid,
                    ...userDocument.data(),
                };
            }
        }
    } catch (error) {
        console.error("Error fetching user", error);
    }
};



export { projectStorage, projectFirestore, timestamp, auth, Providers, generateUserDocument};

The console logs are just so I can work out what is happening. Indeed I get the "happy days" output but after the error above. This is confusing me as it is happening within the onAuthStateChanged function in the useEffect block so why is it not mounted?

I am assuming this is something simple that I am missing. The app works and the person is logged in, but it is annoying that the redirect to the home page does not work. Navigating to the home page manually works.

I believe callback from auth framework is called during navigation or callback is not asynchronous and allows internal routing before is finished. You can check state in callback if component is still mounted.

For example, you can create new new hook

       function useIsMounted(): { current: boolean } {
          const componentIsMounted = useRef(true)
          useEffect(() => {
            return () => { componentIsMounted.current = false }
          }, [])
          return componentIsMounted
        }

You can use this new hook as follows

      const isMounted = useIsMounted();
      useEffect(() => {
        return auth.onAuthStateChanged(async user => {
            ...
            if (isMounted.current) {
                setCurrentUser(usr);
                setLoading(false);
            }
        })
       }, []);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM