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.