I'm attempting to run a function within the useEffect hook, so that on screen load it automatically calls the context and works out what to do.
But for whatever reason, the function just isn't firing. The screen loads successfully and renders, no errors, but just doesn't do anything.
Here's my component I'm calling the context from:
import React, { useContext, useEffect } from 'react';
import { View, StyleSheet, ActivityIndicator } from 'react-native';
import { AuthContext } from '../context/AuthContext';
const LoadingScreen = ({ navigation }) => {
const { userSignedIn } = useContext(AuthContext)
useEffect(() => {
userSignedIn()
}, [])
return (
<View style={styles.mainView}>
<ActivityIndicator style={styles.indicator} />
</View>
)
}
And my context file:
import React, { useState, useContext } from 'react';
import { navigate } from '../navigationRef';
import { Magic } from '#magic-sdk/react-native';
const m = new Magic('API key');
export const AuthContext = React.createContext();
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState([]);
const userSignedIn = () => {
return async () => {
// Call Magic logged in
const loggedIn = await m.user.isLoggedIn();
console.log(loggedIn)
// If user logged in, save details to user, and redirect to dashboard
if (loggedIn === true) {
const { issuer, email } = await m.user.getMetaData();
console.log(issuer)
console.log(email)
setUser([issuer, email])
navigate('authorisedFlow')
// If user not logged in, redirect to login flow
} else {
console.log(userSignedIn)
console.log("Not signed in.")
navigate('loginFlow')
}
}
};
return (
<AuthContext.Provider value={{ user, userSignedIn }}>
{ children }
</AuthContext.Provider>
)
Can anyone point out what I'm doing wrong? Feels a simple one.. But can't figure it out.
You are returning an async function when calling userSignedIn so the following should work for you by making userSignedIn itself async to work for those await calls inside.
const userSignedIn = async () => {
// Call Magic logged in
const loggedIn = await m.user.isLoggedIn();
console.log(loggedIn)
// If user logged in, save details to user, and redirect to dashboard
if (loggedIn === true) {
const { issuer, email } = await m.user.getMetaData();
console.log(issuer)
console.log(email)
setUser([issuer, email])
navigate('authorisedFlow')
// If user not logged in, redirect to login flow
} else {
console.log(userSignedIn)
console.log("Not signed in.")
navigate('loginFlow')
}
};
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 have a React app using Firebase Auth and an Express backend. I have React contexts set up for the user's authentication process and for the loading state of the app. Currently, when a user signs in, the following happens:
The app goes into a loading state
The app sends an API request to the backend to verify the user's token
The backend queries the database and then sets the user's custom claims with their permissions and sends a response with the verified token & claims
The loading state is cleared, and the app becomes useable
The user's routes / nav menu options etc are then determined by the user's permissions according to the backend - i.e, if a user doesn't have permission for a certain area of the site, its routes and nav menu items are not loaded.
My authentication context is as follows:
import { createContext, useContext, useState, useEffect } from "react";
/**
* auth = getAuth()
* provider = new GoogleAuthProvider()
*/
import { auth, provider } from "providers/firebase";
import {
getAuth,
onAuthStateChanged,
signInWithPopup,
signOut as firebaseSignOut
} from "firebase/auth";
import { api } from "providers/axios";
import { useLoading } from "providers/loading";
const UserContext = createContext(null);
export const useAuth = () => useContext(UserContext);
const verifyToken = (token) =>
api({
method: "post",
url: "/user/auth",
headers: {
token
}
});
const UserProvider = (props) => {
const [user, setUser] = useState(null);
const { loading, setLoading } = useLoading();
const signIn = async () => {
setLoading(true);
try {
const result = await signInWithPopup(auth, provider);
console.log("auth signInWithPopup", result.user.email);
} catch (e) {
setUser(null);
console.error(e);
setLoading(false);
}
};
const signOut = async () => {
let userSigningOut = user;
try {
await firebaseSignOut(auth);
setUser(null);
console.log("signed out");
} catch (e) {
console.error(e);
} finally {
return (userSigningOut = null);
}
};
const verifyUser = async (user) => {
try {
if (!user) {
throw "no user";
}
const token = await getAuth().currentUser.getIdToken(true);
if (!token) {
throw "no token";
}
const jwt = await getAuth().currentUser.getIdTokenResult();
if (!jwt) {
throw "no jwt";
}
const verifyTokenResponse = await verifyToken(token);
if (verifyTokenResponse.data.role !== jwt.claims.role) {
throw "role level claims mismatch";
} else {
user.verifiedToken = verifyTokenResponse.data;
console.log(`User ${user.uid} verified`);
setUser(user);
}
} catch (e) {
signOut();
console.error(e);
}
};
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, async (user) => {
setLoading(true);
try {
if (user) {
console.log("onAuthStateChanged", user?.email);
await verifyUser(user);
} else {
throw "no user";
}
} catch (e) {
console.error(e);
} finally {
setLoading(false);
}
});
return unsubscribe;
}, []);
return (
<UserContext.Provider
value={{
signIn,
signOut,
user
}}
>
{props.children}
</UserContext.Provider>
);
};
export default UserProvider;
The problem is that if the user or their permissions are modified, the changes are not reflected in the app until the user performs a hard refresh.
What I'd like to achieve is for the user's token to be re-verified via the backend upon every page change (or similar) and then if their permissions etc have changed, the app then rerenders reflecting the changes. I think this could be achieved by triggering a rerender of a certain part of UserContext after taking it out of the main function, but I'm not sure how to proceed with that.
After #samthecodingman's comment, I added another state for the user's database entry and have achieved the desired outcome with the following changes to UserProvider:
useEffect(() => {
if (user) {
const userDataRef = ref(db, `/users/${user.uid}`);
return onValue(userDataRef, async snapshot => {
await verifyUser(user);
setUserData(snapshot.val());
})
}
}, [user]);
return (
<UserContext.Provider
value={{
signIn,
signOut,
user,
userData
}}
>
{props.children}
</UserContext.Provider>
);
I have a signup/login workflow in React (NextJS), and everything is working correctly; i made a custom hook to remotely check if the user is authenticated based on localStorage jwt:
import React, { useState, useEffect } from 'react';
import axios from '../lib/api';
const useUser = () => {
const [logged, setIsLogged] = useState(false);
const [user, setUser] = useState('');
useEffect(async () => {
const jwt = localStorage.getItem('jwt');
if (!jwt) return;
await axios
.get('/api/users/me', {
headers: {
Authorization: `Bearer ${jwt}`,
},
})
.then((response) => {
setUser(response.data);
setIsLogged(true);
})
.catch((error) => {});
}, []);
return [logged, user, setIsLogged];
};
export default useUser;
This hooks works corectly in 99% of cases, but when i go on login form page, the login form flashes for a sec to logged in users, since the logged status is false before the check is initialized
import React, { useEffect, useState } from 'react';
import useUser from '../../lib/useUser';
import { useRouter } from 'next/router';
import LoginForm from '../../components/LoginForm';
function Login() {
const { push } = useRouter();
const [logged] = useUser();
console.log(ljwt, logged);
if (logged) {
//push('/');
return <p>nored</p>;
}
if (!logged) {
return <LoginForm />;
}
}
export default Login;
how can i avoid this? i tried to pass to useUser the jwt, so it assume the user is logged in while performing the remote check, but it is not really working as expected.
any suggestion?
Don't render the login form when the login state is still indeterminate.
By the way, useEffect functions can't be async in themselves, since they need to either return nothing or a cleanup function; async functions always return a promise.
async function getLoginState() {
const jwt = localStorage.getItem("jwt");
if (!jwt) return [false, null];
const resp = await axios.get("/api/users/me", {
headers: {
Authorization: `Bearer ${jwt}`,
},
});
return [true, response.data];
}
/**
* Get user login state.
*
* Returns undefined if the login state is not yet known.
* Returns a 2-item array [loginState, user] otherwise.
* `user` can be null when `loginState` is false.
*/
function useLoginState() {
const [loginState, setLoginState] = useState(undefined);
useEffect(() => {
getLoginState().then(setLoginState);
}, []);
return loginState;
}
function Login() {
const { push } = useRouter();
const loginState = useLoginState();
if (loginState === undefined) {
return <>Loading...</>;
}
const [logged, user] = loginState;
if (logged) {
return <p>Hi, {JSON.stringify(user)}</p>;
} else {
return <LoginForm />;
}
}
I'm currently building a firebase login system with a verified email screen.
My problem is that I have a reload user button on the verified email screen that updates the user's credentials so that my root directory redirects the user to the AppStack if currentUser.emailVerified === true.
but the reload button isn't being triggered once pressed so that my root directory is still currentUser.emailVerified === false and not redirecting the user to the AppStack.
Login-System/context/AuthContext.js:
import React, { createContext, useContext, useState, useEffect } from 'react';
import { auth } from '../config';
const AuthContext = createContext();
export function useAuth() {
return useContext(AuthContext);
}
export function AuthProvider({ children }) {
const [currentUser, setCurrentUser] = useState();
const [loading, setLoading] = useState(true);
function sendVerification() {
return currentUser.sendEmailVerification();
}
const getUser = () => auth.currentUser;
const reloadUser = () => getUser().reload();
const reload = async () => {
try {
await reloadUser();
const user = getUser();
setCurrentUser(user);
} catch (error) {}
return reload;
};
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged(user => {
setCurrentUser(user);
setLoading(false);
});
return () => {
unsubscribe();
};
}, []);
const value = {
currentUser,
loading,
reload,
sendVerification,
};
return (
<AuthContext.Provider value={value}>
{!loading && children}
</AuthContext.Provider>
);
}
Login-System/screens/VerifyEmailScreen.js:
import React from 'react';
import { Text, StyleSheet } from 'react-native';
import { useAuth } from '../contexts/AuthContext';
import { View, Button } from '../components';
import { Colors } from '../config';
export const VerifyEmailScreen = () => {
const { currentUser, reload, sendVerification } = useAuth();
const handleSendVerification = async () => {
try {
await sendVerification();
} catch (error) {}
return handleSendVerification;
};
return (
<>
<View isSafe style={styles.container}>
<View style={styles.center}>
<Text style={styles.screenTitle}>Check your email</Text>
<Text style={styles.screenInfo}>{currentUser.email}</Text>
<Text style={styles.screenInfo}>
We sent you an email with instructions on how to verify your email
address. Click on the link in the email to get started.
</Text>
<Button
style={styles.button}
onPress={() => handleSendVerification()}>
<Text style={styles.buttonText}>Resend</Text>
</Button>
<Button style={styles.button} onPress={reload}>
<Text style={styles.buttonText}>Done</Text>
</Button>
</View>
</View>
</>
);
};
I did some similar works to check if the user email is verified or not you can use this function:
export function getUserEmailVerified() {
const user = firebase.auth().currentUser;
return !!user ? (user.emailVerified ? 'Yes' : 'No') : 'No';
}
To trigger an email verification you can use this method. This method will trigger an email verification and refresh the user.
export function verificationEmail(email, onSuccess) {
refreshUser();
const user = firebase.auth().currentUser;
const finishAction = message => {
onSuccess();
showToast(message);
};
user
.sendEmailVerification()
.then(() => {
refreshUser();
})
.catch(error => finishAction(error.message));
}
And to refresh the user you can use this method.
export function refreshUser() {
let user = firebase.auth().currentUser;
if (!!user && !user?.emailVerified) {
interval = setInterval(() => {
user?.reload().then();
}, 3000);
}
}
You also need to use onUserChanged from firebase to detect whether user information is changed or not. It returns a listener if your user email verified field is changed anyway it will get here is an example.
export function onUserChanged(listener) {
firebase.auth().onUserChanged(listener);
}
New to react and hooks, I am trying to do a login module using hooks. However when I am not able to update the state of my Auth state. Read elsewhere that useState do not update immediately and needs to be coupled with useEffect() to have it updated. However I am using useState in a custom hook and not sure how to have updated either through useEffect or other means.
Am I doing some kind of anti-pattern here? Anyone able to help?
const useAuth = () => {
const [auth, setAuth] = useState( {} );
useEffect(()=>{
console.log(auth)
},[])
return {auth, setAuth}
}
export const useHandleLogin = (props) => {
const {auth, setAuth} = useAuth()
const history = useHistory();
useEffect(()=>{
console.log(auth)
},[])
const login = () => {
console.log('login action called---- ');
/* check if user is login */
if(localStorage.getItem('user')){
console.log('got user' );
} else {
console.log('no user, calling backend to authenticate... ' );
// change to login api
axios.get(`http://localhost:3001/projects`)
.then(res => {
console.log('call api' /* + JSON.stringify(res) */);
})
localStorage.setItem('user', JSON.stringify({username:'abc',role:'123'}))
console.log('login done' + JSON.stringify(auth));
console.log('login done2' + auth.authenticated);
}
setAuth({
authenticated: true,
displayName: 'My Name',
email: 'xxx#abc.com',
role: 'admin'
})
console.log("sending to success page" + auth + JSON.stringify(auth)) // Error is here. output is : sending to success page[object Object]{}
import React, {useEffect} from "react";
import { useHandleLogin } from "./LoginUtil"
const TestLoginPage = () => {
const { auth, login } = useHandleLogin();
const Clogin = () => {
console.log('auth: ' + auth)
login();
};
return (
<React.Fragment>
<div>login</div>
<button onClick={Clogin} > Login </button>
</React.Fragment>
);
}
export default TestLoginPage;
Seems you are missing to add some dependency in useEffect() dependency array. Also, in order to generate new instance of a callback like login() you should wrap it in useCallback() and add necessary dependencies in the array. From the look of it,
useEffect() in missing auth in dependency array
login() should be wrapped in useCallback() and must have auth in dependency
array
Clogin() must be wrapped in useCallback() and must have auth and login
in dependency array
You can use add eslint-plugin-react-hooks which can help you in prompting warnings if you miss dependency.
After couple of days of trying and reading up, below is what I believe to be the correct implementation.
import React, { useState, useEffect } from "react";
import { useHistory } from "react-router-dom";
import { Redirect } from "react-router-dom";
import axios from 'axios';
export const useAuth = (props) => {
/* const init = localStorage.getItem('user1')? true : false */
const init = {authen : false}
const [auth, setAuth] = useState(init);
const history = useHistory();
console.log("auth initial " + auth)
const checkAuthStatus = () => {
return !!auth.authen
}
const login = () => {
console.log('login action called---- ');
let success = true
if(success){
setAuth({authen : true})
}else{
setAuth ({authen : false})
}
console.log('push history==========' )
/* history.push('/testLoginOK');
history.go(); */
}
const logout = () => {
console.log('logout action called---- ');
setAuth ({authen : false})
history.push('/testLogin');
history.go();
}
useEffect(() => {
console.log("useEffect auth "+auth.authen)
if(!!auth.authen){
/* history.push('/testLoginOK');
history.go(); */
}
})
return {auth, checkAuthStatus, login, logout}
}
/* export default useAuth */