How to disable address bar link after login - javascript

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.

Related

persist username value after page refresh in React using local storage

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;

How can I minimize firestore reads in the "setUsername" scenario?

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;
}, []);
/* ... */
}

How to prevent useEffect() run twice after running a function in context consumer and prevent useContext() to re-render

I'm learned that React will re-render after state changed e.g. setState from useState(), calling the function or variable from useContext() variable. But now I'm don't understand that why I get the ESLint warning call the context function inside the useCallback() without dependency in the list. If I put the dependency in the list, useCallback() will be re-rendered and useEffect() dependency from useCallback() variable will do again. So how to fix the react-hooks/exhaustive-deps when calling the function inside the useContext() variable?
Auth.js
import React, { createContext, useContext, useEffect, useMemo, useState } from "react";
import * as AuthAPI from "../API/AuthAPI"
import Loading from "../Page/Loading"
const AuthContext = createContext()
export const AuthProvider = ({children}) => {
const [user,setUser] = useState()
const [loadingInitial,setLoadingInitial] = useState(true)
useEffect(()=>{
AuthAPI.getCurrentUser()
.then((user)=>setUser(user))
.catch((error)=>{console.log(error)})
.finally(()=>setLoadingInitial(false))
},[])
const login = async (email,password) => {
const user = await AuthAPI.login({email,password})
setUser(user)
return user
}
const register = async (firstname,lastname,email,password) => {
const user = await AuthAPI.register({firstname,lastname,email,password})
setUser(user)
return user
}
const logout = async () => {
const response = await AuthAPI.logout()
setUser(undefined)
}
const value = useMemo(()=>({
user,
setUser,
login,
register,
logout
}),[user])
return (
<AuthContext.Provider value={value}>
{loadingInitial ? <Loading/> : children}
</AuthContext.Provider>
)
}
export const useAuth = () => {
return useContext(AuthContext)
}
Logout.js
import { useCallback, useEffect, useState } from "react";
import { Navigate, useLocation, useNavigate } from "react-router-dom";
import { useAuth } from "../Hooks/Auth";
import * as AuthAPI from "../API/AuthAPI"
import Loading from "./Loading";
function Logout() {
const auth = useAuth()
const location = useLocation()
const navigate = useNavigate()
const [isLoggedOut,setIsLoggedOut] = useState(false)
const logout = useCallback(async () => {
console.log("Logging out!")
await AuthAPI.logout()
auth.setUser((prevState)=>(undefined))
setIsLoggedOut(true)
},[auth]) // --> re-rendered bacause `auth` context in re-rendered when set `user` state.
useEffect(()=>{
logout()
},[logout]) // --> this also to run again from `logout` callback is being re-rendered.
if (!isLoggedOut) {
return <Loading/>
}
return (
<Navigate to="/login" replace/>
)
}
export default Logout
Any help is appreciated.
How about destructuring your auth context, since you are only using setUser inside useEffect?
const { setUser } = useAuth()
useEffect(() => {
....
}, [setUser])
There is no need for creating a memoized logout callback function if logout isn't used/passed as a callback function. Just apply the logging out logic in the useEffect hook.
Render the Loading component and issue the imperative redirect from the resolved Promise chain of the return AuthAPI.logout Promise.
Example:
import { useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { useAuth } from "../Hooks/Auth";
import * as AuthAPI from "../API/AuthAPI"
import Loading from "./Loading";
function Logout() {
const auth = useAuth();
const navigate = useNavigate();
useEffect(() => {
console.log("Logging out!");
AuthAPI.logout()
.then(() => auth.setUser(undefined))
.finally(() => navigate("/login", { replace: true }));
}, []);
return <Loading />;
}
export default Logout;
Can you try to replace your useEffect code into this:
useEffect(logout, [])

Firebase useAuth() context in App.js is undefined?

I have a AuthProvider and useAuth method that returns useContext(AuthContext) inside my App.js root file, however, if I console.log the return value of useAuth() in App.js, it is undefined, why??
In any other child component like Loginpage, there is an authContext displayed..
my App.js:
import { AuthProvider, useAuth } from './providers/AuthProvider'
function App() {
const auth = useAuth()
console.log({ auth }) //undefined??
AuthProvider.js:
import React, { useContext, useState, useEffect } from 'react'
import { app, auth, firestore, storage } from '../firebase'
const AuthContext = React.createContext()
export function useAuth() {
return useContext(AuthContext)
}
export function AuthProvider({ children }) {
const [currentUser, setCurrentUser] = useState(undefined)
const [loginError, setLoginError] = useState(null)
const login = async (email, password) => {
try {
return auth.signInWithEmailAndPassword(email, password)
} catch (error) {
setLoginError(error)
}
}
const logout = () => auth.signOut()
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged(user => {
setCurrentUser(user)
})
return unsubscribe
}, [])
const value = {
loginError,
login,
signup,
logout,
currentUser,
app,
firestore,
storage,
auth,
}
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
}
Since you import AuthProvider in same file of App it gives a clue that you didn't use the Context API correctly.
Context values are available for Context-Consumers.
Context-Consumers are children of Context-Provider, therefore your App component have to be a child of <AuthContext.Provider>:
<AuthProvider>
<App />
</AuthProvider>

TypeError: currentUser is null in firebase react

I found different already answered questions to my question, but the don't help.
I use a custom context to call the firebase.auth().onAuthStateChanged() and set the currentUser.
import React, { useState, useEffect } from "react";
import app from "../firebase";
export const AuthContext = React.createContext();
export const AuthProvider = ({ children }) => {
const [currentUser, setCurrentUser] = useState(null);
useEffect(() => {
app.auth().onAuthStateChanged(setCurrentUser);
}, []);
return (
<AuthContext.Provider value={{ currentUser }}>
{children}
</AuthContext.Provider>
);
};
In my component I call the AuthContext and the currentUser:
import React, { useContext, useEffect, useState } from "react";
import app from "./firebase";
import { AuthContext } from "./Auth/Auth";
function MyComponent() {
const [invoices, setInvoices] = useState([]);
const { currentUser } = useContext(AuthContext);
const getInvoices = () => {
const database = app.firestore();
const unsubscribe = database
.collection("invoices")
.where("uid", "==", currentUser.uid) // HERE currentUser IS NULL
.orderBy("date", "desc")
.onSnapshot((snapshot) => {
setInvoices(
snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() }))
);
});
return () => {
unsubscribe();
};
};
useEffect(() => {
getInvoices();
}, []);
return (<> ... </>);
}
export default MyComponent;
I believe my issue has something to do with promises and the user is not yet loaded. But still I don't know what to do here.
The potential issue could be the value of currentUser returns a bit later so you need to add an extra check in your MyComponent component.
I would add null check for currentUser and extend the dependency array as:
useEffect(() => {
if (currentUser) {
getInvoices();
}
}, [currentUser]);
Probably in the first round the useEffect callback was running once currentUser was still null.

Categories

Resources