the code I have now created is working, but I think it is somehow suboptimal, as it does to many database reads
As far as I understand it, the "onAuthStateChange" can be understood like an useEffect hook, which gets called whenever the user authentication state changes (login, logout). Whenever this happens, the database should be checked for the username which has been chosen by the user. But if I take a look at the console the docSnap is logged a fair amount of times which to me indicates that the function gets called more often than just when the user logs in / logs out.
Context Component
import { createContext } from "react/cjs/react.production.min";
import { onAuthStateChanged } from "firebase/auth";
import { doc, getDoc } from "firebase/firestore";
import { auth,db } from "./firebase";
import { useState, useEffect } from "react";
export const authContext = createContext({
user: "null",
username: "null",
});
export default function AuthenticationContext(props) {
const [googleUser, setGoogleUser] = useState(null);
const [username, setUsername] = useState(null);
const [userID, setUserID] = useState(null);
onAuthStateChanged(auth, (user) => {
if (user) {
setGoogleUser(user.displayName);
setUserID(user.uid);
getUsername();
} else {
setGoogleUser(null);
}
});
const getUsername = async () => {
const docRef = doc(db, `users/${userID}`);
const docSnap = await getDoc(docRef);
if(docSnap.exists()){
setUsername(docSnap.data().username);
}
else{
setUsername(null);
}
};
return (
<authContext.Provider value={{ user: googleUser, username: username }}>
{props.children}
</authContext.Provider>
);
}
What is more, is that when I login with google and submit a username, the components do not get reevaluated - so a refresh is necessary in order for all the changes to take effect, this has something to do with me not updating state in the submitHandler of the login page. If you have some ideas on how I can do this more professionally please let me hear them. Thank you in advance!
Submit Handler on Login Page
const submitHandler = async (event) => {
event.preventDefault();
console.log(auth.lastNotifiedUid);
await setDoc(doc(db, "users", auth.lastNotifiedUid), {
displayName: user,
username: username,
});
await setDoc(doc(db, "usernames", username), { uid: auth.lastNotifiedUid });
};
As pointed out in the comments by Dharmaraj, you set multiple subscriptions to the authentication state. This is because you call onAuthStateChanged in the body of your component, so it is executed on every render.
To avoid this, you should wrap the function in useEffect so that you only subscribe on component mounting, and unsubscribe on unmounting:
export default function AuthenticationContext(props) {
/* ... */
React.useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (user) => { /* ... */ });
return unsubscribe;
}, []);
/* ... */
}
Related
I have been trying to setup authentication on my project for a while using firebase authentication. I am doing a Next.js project.
I am having issues where If I login and refresh the page, I have to login again, I found a partial solution which involves using contexts, but whenever I use it to login, I am getting type error login is not a function. I have attached 3 files I am using
here is my context file
import { createContext, useContext } from "react";
import { useFirebaseAuth } from "../firebase";
const authUserContext = createContext({
authUser: null,
loading: true,
login: async () => {},
});
export function AuthUserProvider({ children }) {
const Auth = useFirebaseAuth;
return (
<authUserContext.Provider value={Auth}>{children}</authUserContext.Provider>
);
}
export const useAuth = () => useContext(authUserContext);
Here is my login page. If i just make the login as a regular export in the firebase Auth file, I dont get the error, i am able to login but the context doesnt get updated.
import React from "react";
import { useState } from "react";
// import { emailRegex } from "../components/Utilities/validations";
import { useAuth } from "../components/contexts/userContext";
const Login = () => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const { login, authUser } = useAuth();
const [loading2, setLoading] = useState(false);
const handleSubmit = async (e) => {
// validate(e.values);
e.preventDefault();
setLoading(true);
try {
const user = await login(email, password);
console.log(user.user);
console.log(authUser);
} catch (err) {
console.log(err);
}
setLoading(false);
};
};
export default Login;
and here is the firebase code I have. If I remove the login function from the scope of the useFirebaseAuth, I can just import the login normally and get a login token. However, it doesnt update the authUser. If I refresh the page, I have to login again.
function useFirebaseAuth() {
const [authUser, setAuthUser] = useState(null);
const [loading, setLoading] = useState(true);
const formatAuthUser = (user) => ({
uid: user.uid,
email: user.email,
});
const authStateChanged = async (authState) => {
if (!authState) {
setAuthUser(null);
setLoading(false);
return;
}
setLoading(true);
const formattedUser = formatAuthUser(authState);
setAuthUser(formattedUser);
console.log("useAuth " + authUser?.email);
};
const login = (email, password) => {
const promise = auth.signInWithEmailAndPassword(auth, email, password);
console.log(promise);
return promise;
};
useEffect(() => {
const unsub = auth.onAuthStateChanged(authStateChanged);
return unsub;
}, []);
return {
authUser,
loading,
login,
};
}
I am trying to set and retrieve the value of username using the local storage. When the page loads, the user should see a heading that says welcome John. And when the page reloads, I need the value to persist.
But when I load the page, I get the error message username is not defined. This is the code, what am I missing?
import React, { useState, useEffect } from "react";
const UserHeading = () => {
const [user, setUser] = useState("john")
useEffect(() => {
JSON.parse(localStorage.getItem(user)) || [];
})
useEffect(() => {
localStorage.setItem(username, JSON.stringify(user));
}, [user]);
console.log(user);
const userid = JSON.parse(localStorage.getItem(user));
console.log(userid);
return <h1> Welcome {userid} </h1>;
};
export default UserHeading;
change your component like this :
import React, { useState, useEffect } from "react";
const UserHeading = () => {
const [user, setUser] = useState("john")
useEffect(() => {
localStorage.setItem('username', JSON.stringify(user)); //changed
}, [user]);
console.log(user)
const userid = JSON.parse(localStorage.getItem('username')) //changed
console.log(userid)
return <h1> Welcome {userid} </h1>;
};
export default UserHeading;
I am trying to create authentication system with react everything is working. I have one private route if there is no user then it redirects to login page.
This is my private route
import React from 'react'
import { Navigate} from 'react-router-dom'
import { useAuth } from '../../context/AuthContext'
export default function PrivateRoute({children}) {
const { currentUser } = useAuth()
if(!currentUser){
return <Navigate to= '/login' />
}
return children;
}
Problem is after login I get redirect to update-profile page but if I enter login link in address bar it logs out and takes user back to login page. I don't know how to deal with that.
This is my context
import React, {useContext, useEffect, useState} from 'react'
import { auth } from '../firebase-config'
const AuthContext = React.createContext()
export function useAuth(){
return useContext(AuthContext)
}
export function AuthProvider({ children }) {
const [currentUser, setCurrentUser] = useState()
const [loading, setLoading] = useState(true)
function singup(email, password){
return auth.createUserWithEmailAndPassword(email, password)
}
function login(email, password){
return auth.signInWithEmailAndPassword(email, password)
}
function logout(){
return auth.signOut()
}
function resetPassword(email){
return auth.sendPasswordResetEmail(email)
}
function updateEmail(email){
return currentUser.updateEmail(email)
}
function updatePassword(password){
return currentUser.updatePassword(password)
}
useEffect(() =>{
const unsubscribe = auth.onAuthStateChanged(user =>{
setCurrentUser(user)
setLoading(false)
})
return unsubscribe
}, [])
const value = {
currentUser,
login,
singup,
logout,
resetPassword,
updateEmail,
updatePassword
}
return (
<AuthContext.Provider value={value}>
{ !loading && children }
</AuthContext.Provider>
)
}
Issue
Since you are manually entering a URL in the address bar, when you do this it will reload the page, which reloads your app. Anything stored in state is wiped. To keep the state you'll need to persist it to longer-term storage, i.e. localStorage.
Solution
Using localStorage you can initialize the currentUser from localStorage, and use a useEffect hook to persist the currentUser to localStorage.
Example:
export function AuthProvider({ children }) {
const [currentUser, setCurrentUser] = useState(
JSON.parse(localStorage.getItem("currentUser"))
);
const [loading, setLoading] = useState(true);
useEffect(() => {
localStorage.setItem("currentUser", JSON.stringify(currentUser));
}, [currentUser]);
...
useEffect(() =>{
const unsubscribe = auth.onAuthStateChanged(user => {
setCurrentUser(user);
setLoading(false);
});
return unsubscribe;
}, []);
const value = {
currentUser,
login,
singup,
logout,
resetPassword,
updateEmail,
updatePassword
};
return (
<AuthContext.Provider value={value}>
{!loading && children}
</AuthContext.Provider>
);
}
When you re-enter a link in the address bar it deletes all your saved context, if you want a simple way to save the login state, you can save the currentUser object in the localStorage(this is a very basic way, not recommended in real websites), and then when the page loads you can use useEffect to get that data from the localStorage and set currentUser to the user you save in the localStorage.
TopBar.js is basically an AppBar component which handles authentication, if a user logs in, I get an object "user", how do I export this "user" object to App.js?
If I manage to export this to App.js, I would like to export it to another component create which can handle adding stuff to my db
I am using React and Firebase
This is my useEffect function
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((authUser) => {
if (authUser) {
// if user has logged in...
setUser(authUser)
if(authUser.displayName){ }
else{
return authUser.updateProfile({
displayName : userName
});
}
} else {
// if user has logged out...
setUser(null);
}
})
return () => {
// perform some cleanup actions
unsubscribe();
}
}, [user, userName]);
In the App.js file, you could create a context that handles this. In that case you have:
const AuthContext = createContext();
This can now be used to wrap the whole app using the Provider component like so:
// the user value comes from the state created
<AuthContext.Provider value={{user}}>
</AuthContext.Provider>
Then you can access this user value from any component using useContext hook:
const { user } = useContext(AuthContext);
// you can go on and access the `user` value anywhere in the component
you can visit this article explaining it in-depth
I want to send user to backend in function handleJoin().
After setUser is called, the initial data not changed.
How to fix it without using class
App.js
import React, { useState } from "react";
import Join from "./components/Join";
const App = () => {
const [user, setUser] = useState({ });
// Send user data to backend
const handleJoin = (input) => {
console.log(input); // > {name: "myname"}
setUser(input); // Not working!
console.log(user); // > { }
// I want to connect backend here
// But the user objet is empty
};
return <Join onJoin={handleJoin} />;
};
export default App;
user will be updated on the next render after calling setUser.
import React, { useState, useEffect } from "react";
import Join from "./components/Join";
const App = () => {
const [user, setUser] = useState(null);
// This is a side effect that will occur when `user`
// changes (and on initial render). The effect depends
// `user` and will run every time `user` changes.
useEffect(() => {
if (user) {
// Connect to backend here
}
}, [user])
// Send user data to backend
const handleJoin = (input) => {
console.log(input);
setUser(input);
};
return <Join onJoin={handleJoin} />;
};
export default App;
State update is not synchronous so it will not update user object right away but it will be updated asynchronously. So Either you can use input which is going to be user value to be sent to backend or you can use useEffect() hook which will be triggered when user value will be udpated
useEffect(() => {
// this will be triggered whenever user will be updated
console.log('updated user', user);
if (user) {
// connect to backend now
}
}, [user]);