I've been chasing my tail for hours now trying to figure out how to handle auth on my component using firebase and react hooks.
I've created a custom useAuth hook that is intended to handle all the auth behaviors. My thought was to put a useEffect on the root of my component tree that would trigger if the firebase.auth.onAuthStateChanged() ever changed (ie, user is now logged out / logged in.) But, at this point after making a million unsuccessful changes I really don't know what I'm doing anymore.
Here is the code that I have...
RootPage component
const RootPage = ({ Component, pageProps }): JSX.Element => {
const { logoutUser, authStatus } = useAuth();
const router = useRouter();
useEffect(() => {
authStatus();
}, [authStatus]);
...
}
my thought was ok, lets trigger authStatus on mount, but that ends up with me lying about my dependencies. So, in an effort to not lie about my deps, I added authStatus to the deps. Logging out and then logging in results in this:
useAuth hook
const useAuth = () => {
const { fetchUser, resetUser, userData } = useUser();
const { currentUser } = firebaseAuth;
const registerUser = async (username, email, password) => {
try {
const credentials = await firebaseAuth.createUserWithEmailAndPassword(
email,
password
);
const { uid } = credentials.user;
await firebaseFirestore
.collection('users')
.doc(credentials.user.uid)
.set({
username,
points: 0,
words: 0,
followers: 0,
following: 0,
created: firebase.firestore.FieldValue.serverTimestamp(),
});
fetchUser(uid);
console.log('user registered', credentials);
} catch (error) {
console.error(error);
}
};
const loginUser = async (email, password) => {
try {
// login to firebase
await firebaseAuth.signInWithEmailAndPassword(email, password);
// take the current users id
const { uid } = firebaseAuth.currentUser;
// update the user in redux
fetchUser(uid);
} catch (error) {
console.error(error);
}
};
const logoutUser = async () => {
try {
// logout from firebase
await firebaseAuth.signOut();
// reset user state in redux
resetUser();
return;
} catch (error) {
console.error(error);
}
};
const authStatus = () => {
firebaseAuth.onAuthStateChanged((user) => {
if (user) {
console.log('User logged in.');
// On page refresh, if user persists (but redux state is lost), update user in redux
if (userData === initialUserState) {
console.log('triggered');
// update user in redux store with data from user collection
fetchUser(user.uid);
}
return;
}
console.log('User logged out.');
});
};
return { currentUser, registerUser, loginUser, logoutUser, authStatus };
};
export default useAuth;
I'm relatively certain that react hooks are only meant for reusable pieces of logic, so if the purpose of your hook is to contact firebase in every single component you're using it, along with rerendering and refreshing state every time that component is updated, then it's fine, but you can't use hooks for storing global auth state, which is how auth should be stored.
You're looking for react context instead.
import React, {createContext, useContext, useState, useEffect, ReactNode} from 'react'
const getJwt = () => localStorage.getItem('jwt') || ''
const setJwt = (jwt: string) => localStorage.setItem('jwt', jwt)
const getUser = () => JSON.parse(localStorage.getItem('user') || 'null')
const setUser = (user: object) => localStorage.setItem('user', JSON.stringify(user))
const logout = () => localStorage.clear()
const AuthContext = createContext({
jwt: '',
setJwt: setJwt,
user: {},
setUser: setUser,
loading: false,
setLoading: (loading: boolean) => {},
authenticate: (jwt: string, user: object) => {},
logout: () => {},
})
export const useAuth = () => useContext(AuthContext)
const Auth = ({children}: {children: ReactNode}) => {
const auth = useAuth()
const [jwt, updateJwt] = useState(auth.jwt)
const [user, updateUser] = useState(auth.user)
const [loading, setLoading] = useState(false)
useEffect(() => {
updateJwt(getJwt())
updateUser(getUser())
}, [])
const value = {
jwt: jwt,
setJwt: (jwt: string) => {
setJwt(jwt)
updateJwt(jwt)
},
user: user,
setUser: (user: object) => {
setUser(user)
updateUser(user)
},
loading: loading,
setLoading: setLoading,
authenticate: (jwt: string, user: object) => {
setJwt(jwt)
updateJwt(jwt)
setUser(user)
updateUser(user)
},
logout: () => {
localStorage.removeItem('jwt')
localStorage.removeItem('user')
updateJwt('')
updateUser({})
setLoading(false)
},
}
return <AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
}
export default Auth
...
// app.tsx
import Auth from './auth'
...
<Auth>
<Router/>
</Auth>
// or something like that
...
import {useAuth} from './auth'
// in any component to pull auth from global context state
You can change that according to whatever you need.
I know the issue why its happening but don't know the solution...But i am not fully sure...Look how react works is if any parents re render it also cause re render the children..ok?Its mean if any reason your apps is re rendering and the useAuth keep firing...so for this there to much console log.But i am not sure that it will work or not..give me your repo i will try on my local computer
const RootPage = ({ Component, pageProps }): JSX.Element => {
const { logoutUser, authStatus,currentUser } = useAuth();
const router = useRouter();
useEffect(() => {
authStatus();
}, [currentUser]);
//only fire when currentUser change
...
}
Update your useEffect hook like so:
useEffect(() => {
const unsub = firebaseAuth.onAuthStateChanged((user) => {
if (user) {
console.log('User logged in.');
// On page refresh, if user persists (but redux state is lost), update user in redux
if (userData === initialUserState) {
console.log('triggered');
// update user in redux store with data from user collection
fetchUser(user.uid);
}
} else {
console.log('User logged out.');
}
});
return ()=> unsub;
},[])
Related
AuthContext.js
import { createContext, useEffect, useState } from "react";
import { axiosInstance } from "../../axiosConfig";
import { useCustomToast } from "../../customHooks/useToast";
const initialState = {
user: null,
isLoggedIn: false,
login: () => null,
logOut: () => null,
};
export const AuthContext = createContext(initialState);
export const AuthContextProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [isLoggedIn, setIsLoggedIn] = useState(false);
const { showToast } = useCustomToast();
console.log("i am rinning agaon here");
const checkLogin = async () => {
try {
const res = await axiosInstance.get("/auth/refresh");
setIsLoggedIn(true);
console.log("the user is", res?.data);
setUser(res?.data?.user);
} catch (e) {
console.log(e);
setIsLoggedIn(false);
}
};
const logOutHandler = async () => {
try {
const res = await axiosInstance.get("/auth/logout");
showToast(res?.data?.message);
} catch (e) {
showToast("Something went wrong.Please try again");
}
};
useEffect(() => {
checkLogin();
}, []);
const login = (userData) => {
setUser(userData);
setIsLoggedIn(true);
};
const logOut = () => {
setUser(null);
logOutHandler();
setIsLoggedIn(false);
};
return (
<AuthContext.Provider
value={{
user,
isLoggedIn,
login,
logOut,
}}
>
{children}
</AuthContext.Provider>
);
};
ProtectedRoute.js
import React, { useEffect, useContext } from "react";
import { useRouter } from "next/router";
import { AuthContext } from "../context/authContext";
const ProtectedRoute = ({ children }) => {
const { isLoggedIn } = useContext(AuthContext);
const router = useRouter();
useEffect(() => {
if (!isLoggedIn) {
router.push("/login");
}
}, [isLoggedIn]);
return <>{isLoggedIn && children}</>;
};
export default ProtectedRoute;
I am using NextJS and context api for managing user state. Here at first I will check for tokens and if it is valid I will set loggedIn state to true. But suppose I want to go to profile page which is wrapped by protected route, what is happening is AuthContext is resetting and evaluating itself from beginning, the isLoggedIn state is false when I go to /profile route. If I console log isLoggedIn state inside protectedRoute.js, it is false at start and before it becomes true, that router.push("/login) already runs before isLoggedIn becomes true. It feels like all AuthContext is executing again and again on each route change. Is there any code problem? How can I fix it? The one solution I have found is wrapping that wrapping that if(!loggedIn) statement with setTimeOut() of 1 secs so that until that time loggedIn becomes true from context API
I am trying to do backend for my website and I have login, signup, reset password etc. all working. What I am trying to do now is when user log in or sign up, AuthContext to check for file that match his UID and if exist store it to variable or if not exist create it and store it to variable. Just cant get it work. Best i got so far is code at bottom but problem there is when I log out user I am getting all sort errors because user not exists anymore.
my context file look like this so far and everything is working:
import { createContext, useContext, useEffect, useState } from "react";
import { auth, db } from "../firebase";
export const AuthContext = createContext();
export const useAuth = () => {
return useContext(AuthContext);
};
const AuthContextProvider = ({ children }) => {
const [currentUser, setCurrentUser] = useState(null);
const [currentUserDoc, setCurrentUserDoc] = useState(null);
const [loading, setLoading] = useState(true);
const signup = (email, password) => {
return auth.createUserWithEmailAndPassword(email, password);
};
const login = (email, password) => {
return auth.signInWithEmailAndPassword(email, password);
};
const logout = () => {
return auth.signOut();
};
const resetPassword = (email) => {
return auth.sendPasswordResetEmail(email);
};
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((user) => {
setCurrentUser(user);
setLoading(false);
});
return unsubscribe;
}, []);
const value = { currentUser, currentUserDoc, signup, login, logout, resetPassword };
return <AuthContext.Provider value={value}>{!loading && children}</AuthContext.Provider>;
};
export default AuthContextProvider;
I tryed to change useEffect hook to this but can't get it done right:
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged(async (user) => {
setCurrentUser(user);
const userDoc = db.collection("users").doc(user.uid);
await userDoc.get().then((doc) => {
if (doc.exists) {
setCurrentUserDoc(doc.data());
} else {
doc.set({
email: user.email,
first_name: "",
last_name: "",
country: "",
organization: "",
});
}
});
setLoading(false);
});
return unsubscribe;
}, []);
This is error when there is no user logged in:
Code where I am trying to use it:
import styles from "./Header.module.scss";
import { useAuth } from "../../contexts/AuthContext";
const Header = () => {
const { logout, currentUserDoc } = useAuth();
return (
<header className={styles.header}>
<div></div>
<div className={styles.header__user}>
{currentUserDoc.email}
<button onClick={logout}>Log out</button>
</div>
</header>
);
};
export default Header;
The onAuthStateChanged observer will trigger when the user logs in or logs out. In case the user has logged out, the user object will be null and hence you will get an error "TypeError: Cannot read property 'uid' of null". You should check if the user is still logged in inside of the auth observer.
const unsubscribe = auth.onAuthStateChanged(async (user) => {
// Check if user is present (logged in) or absent (logged out)
if (!user) {
// user has logged out
console.log("No User")
} else {
// Add the required documents
setCurrentUser(user);
const userDoc = db.collection("users").doc(user.uid);
const doc = await userDoc.get()
if (doc.exists) {
setCurrentUserDoc(doc.data());
} else {
await userDoc.set({
email: user.email,
first_name: "",
last_name: "",
country: "",
organization: "",
});
}
}
})
Can anyone help me with this please with this useEffect in React? I am updating custom claims in firebase auth via a firestore document called user_claims. In here it has a user role. Instead of waiting an hour for the token to be refreshed....I want to refresh it as soon as I make a change to the user_claims collection. Based on the below I am getting the error:
Unhandled Rejection (TypeError): Cannot read property 'getIdTokenResult' of undefined
Have I put my listeners in the wrong order or should I have two useEffects? Not sure what I need to do and would appreciate guidance. I want to get the user role - in real-time into the AuthContext for use throughout the app.
thanks all in advance for your help and guidance, as always.
import React, { useEffect, useState, createContext } from 'react'
import {firebase, firestore} from './firebase'
export const AuthContext = createContext()
export const AuthProvider = ({ children }) => {
const auth=firebase.auth();
const [currentUser, setCurrentUser] = useState();
const [error, setError] = useState();
useEffect(() => {
async function reportAdminStatus() {
const result = await currentUser.getIdTokenResult(false)
const isAdmin = result.claims.isAdmin
if (isAdmin) {
console.log("Custom claims say I am an admin!")
}
else {
console.log("Custom claims say I am not an admin.")
}
}
auth.onIdTokenChanged(user => {
if (user) {
console.log(`new ID token for ${user.uid}`)
setCurrentUser(user)
reportAdminStatus()
}
})
auth.onAuthStateChanged(async (user) => {
function listenToClaims() {
firestore
.collection('user_claims')
.doc(currentUser.uid)
.onSnapshot(onNewClaims)
}
let synced
function onNewClaims(snapshot) {
const data = snapshot.data()
console.log('New claims doc\n', data)
if (data._synced) {
if (synced &&
!data._synced.isEqual(synced)) {
// Force a refresh of the user's ID token
console.log('Refreshing token')
currentUser.getIdToken(true)
}
synced = data._synced
}
}
if (user) {
try {
const idTokenResult = await user.getIdTokenResult();
setCurrentUser({...user, role: idTokenResult.claims.role, isAdmin: idTokenResult.claims.isAdmin, group: idTokenResult.claims.group });
setError(undefined);
listenToClaims();
reportAdminStatus();
} catch (e) {
setError(e);
}
} else {
setCurrentUser(undefined);
}
});
return [currentUser, error]
}, []);
return (
<AuthContext.Provider value={{ currentUser }}>
{children}
</AuthContext.Provider>
)
}
I have a simple useEffect that I'm not sure how to stop from invoking endlessly. It keeps firing the first if conditional endlessly. I've been reading a lot about hooks and I assume (maybe erroneously) that each render of the component results in a new invocation of my useAuth() and useUser() hooks. Since they have new references in memory it's triggering the useEffect's deps since technically it's a new function that exists in the scope of this new component render?
Thats my thought at least, no clue how to fix that if that's indeed that case.
const RootPage = ({ Component, pageProps }): JSX.Element => {
const { logoutUser } = useAuth(); // imported
const { fetchUser } = useUser(); // imported
const router = useRouter();
useEffect(() => {
// authStatus();
const unsubscribe = firebaseAuth.onAuthStateChanged((user) => {
if (user) {
console.log(1);
return fetchUser(user.uid); // async function that fetches from db and updates redux
}
console.log(2);
return logoutUser(); // clears userData in redux
});
return () => unsubscribe();
}, [fetchUser, logoutUser]);
...
}
fetchUser
const fetchUser = async (uid) => {
try {
// find user doc with matching id
const response = await firebaseFirestore
.collection('users')
.doc(uid)
.get();
const user = response.data();
// update redux with user
if (response) {
return dispatch({
type: FETCH_USER,
payload: user,
});
}
console.log('no user found');
} catch (error) {
console.error(error);
}
};
logoutUser
const logoutUser = async () => {
try {
// logout from firebase
await firebaseAuth.signOut();
// reset user state in redux
resetUser();
return;
} catch (error) {
console.error(error);
}
};
when I refresh the page with this useEffect on this is output to the console:
useEffect(() => {
function onAuthStateChange() {
return firebaseAuth.onAuthStateChanged((user) => {
if (user) {
fetchUser(user.uid);
} else {
resetUser();
}
});
}
const unsubscribe = onAuthStateChange();
return () => {
unsubscribe();
};
}, [fetchUser, resetUser]);
Keeping everything the same && wrapping fetchUser and resetUser with a useCallback, this solution seems to be working correctly. I'm not entirely sure why at the moment.
Can't merge ...state and result of function return.
I'm try to changing class component to function component.
so I updated react and used hook.
first of all I want to change class's state, setState to hook's those.
but hook's setState replace oject not merging like class' setState.
It is original code below
import React from 'react'
import produce from 'immer'
import {
getUserFromCookie,
login,
logout,
profile,
updateProfile
} from '../api'
const userInfo = getUserFromCookie()
const UserContext = React.createContext({
...userInfo
})
export const withUserContext = WrappedComponent => {
return class ProviderComponent extends React.Component {
constructor(props) {
super(props)
this.state = {
...userInfo,
consentNeeded: false,
updateConsent: async ({ pi, news, seen }) => {
await updateProfile({ pi, news, seen })
this.setState({
consentNeeded: false
})
},
profile: async () => {
const userProfile = await profile()
if (userProfile.seen_consent_modal === false) {
this.setState({
consentNeeded: true
})
}
},
login: async ({ userId, password }) => {
const user = await login({ userId, password })
this.setState(
produce(draft => {
return user
})
)
},
logout: async () => {
await logout()
}
}
}
render() {
return (
<UserContext.Provider value={this.state}>
<WrappedComponent {...this.props} />
</UserContext.Provider>
)
}
}
}
export default UserContext
and It is function Component I worked.
import React, { useState } from 'react'
import produce from 'immer'
import {
getUserFromCookie,
login,
logout,
profile,
updateProfile
} from '../api'
const userInfo = getUserFromCookie()
const UserContext = React.createContext({
...userInfo
})
export const withUserContext = WrappedComponent => {
return function provideComponent() {
const [state, setState] = useState({
...userInfo,
consentNeeded: false,
updateConsent: async ({ pi, news, seen }) => {
console.error('updateConsent!!')
await updateProfile({ pi, news, seen })
setState({
consentNeeded: false
})
},
profile: async () => {
console.error('profile!!')
const userProfile = await profile()
if (userProfile.seen_consent_modal === false) {
setState({
consentNeeded: true
})
}
},
login: async ({ userId, password }) => {
const user = await login({ userId, password })
setState(
produce(() => user)
)
},
logout: async () => {
await logout()
}
})
return (
<UserContext.Provider value={state}>
<WrappedComponent {...props} />
</UserContext.Provider>
)
}
}
export default UserContext
underline warning.. I think it is not correct syntax
Edit:
I realized what is the problem. I made a codesandbox with everything working (except by the functions you didn't provided).
1. HOCs should be used for Contex.Consumer Not Context.Provider
In your code, you are making a HOC for Context.Provider but the correct way should be for Contex.Consumer.
To work with context, you need
<Contex.Provider>
...
<AnyThingYouWant>
<Context.Consumer>
</Context.Consumer>
</AnyThingYouWant>
</Contex.Provider>
If you want a HOC for Contex.Provider, you only need to use children and wrap it around your components
e.g.
const UserContext = React.createContext('my context')
const UserProvider = (props) => {
const value = useState('someState')
return (
<UserContext.Provider value={value}>
{children}
</UserContext.Provider>
)
}
2. If you are using functional components, you don't need HOC anymore.
React Hooks introduced useContext.
Now the only thing you need to render the Context.Provider and use it like so const {...contextValue} = useContext(MyContext).
e.g.
const { updateConsent, profile, login, logout, ...otherStuff } = useContex(UserContext)
3.Inside Context.Consumer you need to pass a function that render the WrappedComponent
When making a HOC for Context.Consumer, you need to have a function that renders the WrappedComponent and reciveis the props from the consumer.
e.g.
const withUserContext = WrappedComponent => {
return function UserContextHoc(props) {
return (
<UserContext.Consumer>
// function that render the `WrappedComponent`
{consumerProps => <WrappedComponent {...props} {...consumerProps} />}
</UserContext.Consumer>
);
};
};
If you do something like this, it's wrong
<UserContext.Consumer>
// THIS IS WRONG AND WILL THROW AN ERROR
<WrappedComponent {...props} />
</UserContext.Consumer>
If you look at the codesandbox you will see that it gives no errors and also, in the console inside MyComponent, it shows everything that is from the UserContext.
Hope now everything is more clear.
Old:
Your functions should be out side of useState initial value to be able to call setState.
// state has multiple key value
const [state, setState] = useState({
...userInfo,
consentNeeded: false,
})
const updateConsent = async ({ pi, news, seen }) => {
await updateProfile({ pi, news, seen })
setState({
consentNeeded: false
})
}
const profile = async () => {
const userProfile = await profile()
if (userProfile.seen_consent_modal === false) {
// setState(prevState => {
// return {...prevState, {consentNeeded: true}};
// });
setState({
consentNeeded: true
})
}
}
const login = async ({ userId, password }) => {
const user = await login({ userId, password })
// code below change it as produce's result.
// not merging of exist states
// setState(
// produce(() => {
// return user
// })
// )
// what I've tried.. but warning underline..
setState(prevState => {...prevState, produce(() => user)})
}
const logout = async () => {
await logout()
}
return (
<UserContext.Provider value={{
...state,
updateConsent,
profile,
login,
logout,
}>
<WrappedComponent {...props} />
</UserContext.Provider>
)