I have an Authentication Context that uses useEffect for getting data from sessionStorage and set a global user variable to pass down via context api.
On each protected route, I have a useEffect inside my hoc to check if the user is logged, and if it isn't send the user back to login page.
However, the useEffect inside the hoc is firing before the Authentication Context and therefore, doesn't see the authentication data and sends the customer to login page every time
const router = useRouter()
const [ user, setUser ] = useState(null)
const [ loading, setLoading ] = useState(false)
useEffect(() => {
async function loadUserFromSessionStorage() {
const token = sessionStorage.getItem('accessToken')
if (token) {
const { data: { customer: { name } } } = await axios.get(`http://localhost:3002/customer/token/${token}`)
if (name) setUser(name)
}
setLoading(false)
}
loadUserFromSessionStorage()
})
useEffect(() => {
if(!!user) router.push('/')
}, [ user ])
return (
<AuthContext.Provider
value={{ isAuthenticated: !!user, loading, user}}
>
{children}
</AuthContext.Provider>
)
}
And this is my HOC:
return () => {
const { user, isAuthenticated, loading } = useContext(AuthContext);
const router = useRouter();
useEffect(() => {
if (!isAuthenticated && !loading){
router.push("/login")
}
}, [ loading, isAuthenticated ]);
return (
isAuthenticated && <Component {...arguments} />
)
};
}
Does anyone know how to solve this?
As you may or may not know, the useEffect hook in your HOC will fire before the hook that loads your user from session state completes.
Where you went wrong is in setting your loading state to false by default:
const [ loading, setLoading ] = useState(false)
When you do this in the HOC effect hook
if (!isAuthenticated && !loading)...
That expression will be true the first time through and you get redirected to the login page. Just do useState(true) instead.
You're not passing the loading variable into the context, so when you deconstruct the context value in your HOC, it looks like:
{
user: undefined,
isAuthenticated: false,
loading: undefined
}
which, based on your logic will redirect to login.
Try adding the other two variables inside your AuthContext.Provider and see if that helps you out.
Related
I am using React-query for my React app.
I have useLogin and useLogout hooks which use useQuery:
export const useLogin = (input?: LoginWithEmailPassword) => {
const { data, isLoading, isSuccess, error } = useQuery(
["loginWithEmailPassword"],
() => loginWithEmailPassword(input),
{
enabled: !!input,
}
);
return { data: data?.data, isLoading, isSuccess, error };
};
export const useLogout = (accessToken?: string) => {
const { isSuccess, isLoading, error } = useQuery(
["logout"],
() => logout(accessToken),
{
enabled: !!accessToken,
}
);
return { isSuccess, isLoading, error };
};
In my AuthProvider; where I use the 2 hooks, I also have a login and logout function which will be called when a user clicks login/logout.
Given React-query's declarative approach, I'm also keeping 2 states; loginInput and logoutInput
AuthProvider.tsx
const [loginInput, setLoginInput] = useState<LoginWithEmailPassword>();
const [logoutInput, setLogoutInput] = useState<string | undefined>();
const { data, isSuccess: isLoginSuccess, isLoading } = useLogin(loginInput);
const {...} = useLogout(logoutInput);
const login = (input: LoginWithEmailPassword) => {
setLoginInput(input);
};
const logout = () => {
setLogoutInput(data?.accessToken);
};
This issue I've found is that after the user clicks login and set's the loginInput state; useLogin will run. But useLogin will re-run every time the component re-rerenders because loginInput will still have the state; i.e. if a user clicks logout, useLogin will run again. What would be the best way to resolve this?
Things will be more straightforward if React-query has a useLazyQuery like Apollo.
A hacky approach I can think of is to reset the loginInput state to undefined, like so:
useEffect(()=>{
if(isLoginSuccess && !isLoading && !!data) setLoginInput(undefined)
},[isLoginSuccess])
login and logout are not queries, but mutations. They are not idempotent - you can't run them at will. They change some state on the server (logging the user in) or create a resource (a login token).
So, the answer is: don't use queries here.
I'm trying to put in a layer of authentication in my protected routes and I've been doing this using the following:
function ProtectedRoute ({ children }) {
const { user } = useContext(AuthContext);
return user.isVerifying ? (
<p>Is loading...</p>
) : user.verified ? children : <Navigate to="/login" />;
}
export default ProtectedRoute;
My user context is initialised with the following information:
{
"user": null,
"verified": false,
"isVerifying": false
}
At the moment, the user authorisation state is checked by a function (authUser), inside of my AuthContext.Provider, that at the moment runs on every render of my components (shown below). This is an asynchronous function that makes an API call to check to see if the user JWT token stored in localStorage is valid.
import React, { createContext, useEffect, useState } from 'react';
import axios from 'axios';
const AuthContext = createContext();
function AuthProvider({children}) {
const [user, setUser] = useState({
"user": null,
"verified": false,
"isVerifying": true
});
async function authUser() {
const userLocal = localStorage.getItem('user');
if (userLocal) {
const userLocalJSON = JSON.parse(userLocal);
const verifyResponse = await verifyToken(userLocalJSON);
if (verifyResponse) {
setUser({"user": userLocalJSON, "verified": true, "isVerifying": false});
} else {
setUser({"user": userLocalJSON, "verified": false, "isVerifying": false});
}
}
}
async function verifyToken(userJSON) {
try {
setUser({"user": null, "verified": false, "isVerifying": true});
// Make API call
if (response.status === 200) {
return true;
} else {
return false;
}
} catch (e) {
return false;
}
}
useEffect(() => {
authUser();
}, [])
return (
<AuthContext.Provider value={{user, authUser}}>
{children}
</AuthContext.Provider>
)
}
export default AuthContext;
export { AuthProvider };
Upon validating the token, the user context is then updated to verified = true with the information about the user. During this process, the user context isVerifying = true (an equivalent to isLoading) and gets set back to false once the API call is complete.
Now there are a few problems with this:
On every re-render of the DOM, the context information is set back to its defaults. Is there a way around this so that useEffect() only fires upon localStorage changes? I've tried implenting the following solution, which adds a listener to localStorage and then triggers the useEffect when that changes. I could not get this to fire upon localStorage changes. Maybe I did something wrong (see below)?
useEffect(() => {
window.addEventListener('storage', authUser)
return () => {
window.removeEventListener('storage', authUser)
}
}, [])
Because the initial isVerifying = false and the switch to true doesn't occur before the DOM is rendered, the protected route navigates to the login page. Even after the user context is finally set, it remains on the login page. The loading page is never displayed and instead there is an instant redirection to the login page. Unfortunately, I can't set isVerifying = true initially as non-logged in users get stuck on the loading page.
Any suggestions would be really helpful.
To solve question 2:
Your ternary isn't quite doing what you want. It's kind of like saying "if user.isVerifying or !user.verified, navigate". That's not what we want.
Embedded ternaries can be tricky and I usually try to avoid them if I can. Since this component is so small, you don't need to do this with ternaries at all actually. I think this should work:
Set isVerifying to start out true. Then:
function ProtectedRoute ({ children }) {
const { user } = useContext(AuthContext);
if (user.isVerifying) {
return <p>Is loading...</p>
}
if (user.verified === false) {
return <Navigate to="/login" />
}
return children
}
export default ProtectedRoute;
EDIT: You also need to update the verification state in authUser. Noticed how you are only updating the loading state if userLocal is truthy. Best bet is to start out loading, and always call setUser at the end. This isn't the only way to do it but I think it should work:
async function authUser() {
setUser(currentState => {...currentState, isVerifying: true})
const userLocal = localStorage.getItem('user');
if (!userLocal) {
setUser({
user: null,
verified: false,
isVerifying: false,
})
}
const userLocalJSON = JSON.parse(userLocal);
const verifyResponse = await verifyToken(userLocalJSON);
setUser({
user: userLocalJSON,
verified: verifyResponse,
isVerifying: false,
})
}
I have react native app and i am implementing auth flow using Context API
the nature of the app is when user open it won't request to login or signup and user can explore the app screens however when user add items to cart and about to checkout then will be requested to sign-in/sign-up to continue.
let's assume these are the screens
Home-> shop -> cart
so when user in the cart will be asked to login, after login user value in context provider will be updated and they continue from same screen the user logged in (which in this case is cart screen)
However, when provider value updated all screens re-render again and redirect to initial route which home screen.
how can i handle this scenario which not re-render all screen again.
In my app i am passing the navigation stuck as a children to context provider as follow:
<AuthProvider>
<Routes />
</AuthProvider>
and here is the code of context provider:
authProvider.js:
export const AuthContext = createContext();
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const value = useMemo(
() => ({ user, setUser }),
[user]);
return (
<AuthContext.Provider
value={{
value,
login: async (email, password) => {
//here i am validating the login details and update user state
// if login successful
setUser(/*some data related to user */)
//when i set user data here it re-render all screens again
// if login failed
setUser(null)
}
}}>
{children}
</AuthContext.Provider >
and the following the Routes.js which i am passing it as children to the above code:
const Routes = () => {
const { value } = useContext(AuthContext);
// validate token with backend
useEffect(() => {
setTimeout(async () => {
const Token = await AsyncStorage.getItem('Token');
if (Token === null) {
value.setUser(null)
} else {
await axios.get(URL, {
headers: { 'x-auth-token': Token },
}).then(async (res) => {
const {data} = res;
value.setUser(data)
}).catch(async (err) => {
if (err.response) {
await AsyncStorage.removeItem('Token');
value.setUser(null)
}
});
}
}, 1000)
}, []);
return (
<>
<NavigationContainer>
//this is the stuck where i have the three screen mentioned above (home, shop, cart)
<AppStuckScreen />
</NavigationContainer>
</>
)
}
export default Routes;
In cart screen i am checking the user values stored in context provider if null then open the login form.
I hope someone can help me on how to handle this situation where not to re-render all component again when updating user value in Context provider.
Why don't you keep the current screen in the Context? So after a successful login, you just navigate to that screen.
The situation is as follows. In my application, I use a router, and pass information to it whether the user is identified or not, similar to the role of the administrator. The data is stored in the auth context, put there auth hook, and used in App.js. The entire code is below. The problem is that when I reload the page, I get redirected from any tab to the home page. This happens because of a momentary change in App.js when constructing the App component, the variables in the useRoutes(isAuthenticated, admin) function change to false and true(true - after identification) when the page is reloaded. I'm relatively new to React, and don't really understand how to solve this issue. All I want to achieve is to make sure that the variables don't change their values in the App by simply refreshing the page.
App.js
function App() {
const { token, login, logout, admin } = useAuth()
const isAuthenticated = !!token
const routes = useRoutes(isAuthenticated, admin)
return (
<AuthContext.Provider value={{
token, login, logout, isAuthenticated, admin
}}>
<Router>
<div className="app-container">
{routes}
</div>
</Router>
</AuthContext.Provider>
);
}
AuthContext.js
function noop() { }
export const AuthContext = createContext({
token: null,
login: noop,
logout: noop,
isAuthenticated: false,
admin: false,
})
Auth.hook.js
const storageName = 'userData'
export const useAuth = () => {
const [token, setToken] = useState(null)
const [admin, setAdmin] = useState(false)
const login = useCallback((jwtToken, isAdmin) => {
setToken(jwtToken)
setAdmin(isAdmin)
localStorage.setItem(storageName, JSON.stringify({
token: jwtToken,
isAdmin: isAdmin,
}))
}, [])
const logout = useCallback(() => {
setToken(null)
setAdmin(false)
localStorage.removeItem(storageName)
}, [])
useEffect(() => {
const data = JSON.parse(localStorage.getItem(storageName))
if (data && data.token) {
login(data.token, data.isAdmin) // <-- There some problem
}
}, [login])
return { login, logout, token, admin }
}
I solved this problem with a simple solution :)
All you need to do is just add 1 more variable ready in the context with a value of false . Then set it in auth.hook.js to true after login. And export it to App.js and use it like if(ready){return "page"} else return <>Loading</>.
So I have Dashboard component which should display currentUsers info.
I keep my currentUser in global context/state.
The problem is when Dashboard component renders FIRST time currentUser is null even tho user is actually logged in. Because of that I get null error at line 14 or if I comment dashboard states the if statement on line 22 will happen and it will redirect me even if user is logged in.
Is there a way to await for isLoggedInFunction to finish and then do the logic in Dashboard component?
In App.js
export const CurrentUserContext = React.createContext(null); // Global Context
const [currentUser, setCurrentUser] = useState(null); // Global State
// Setting current user
useEffect(() => {
setCurrentUser(isLoggedIn()).catch(err => console.log(err));
}, []);
// IsLoggedIn Method
export function isLoggedIn() {
return localStorage.getItem('user') === null ?
null :
JSON.parse(localStorage.getItem('user'));
}
<CurrentUserContext.Provider value={{ currentUser, setCurrentUser }}> // Provider
You can set the default value for the user to avoid the first error.
In this case you can change your Dashboard like this:
/* ... */
const {
currentUser = {
isLoggedIn: false,
firstName: 'first name'
}
} = useContext(CurrentUserContext);
const [firstName, setFirstName] = useState(currentUser.firstName);
/* ... */
if (!currentUser.isLoggedIn) {
return <Redirect to={'/login'}/>;
}
/* ... */
To solve the second problem, you need to check if your localStorage actually contains the user field after authorization.
I fixed both problems by doing this.
const [currentUser, setCurrentUser] = useState(null); // Replaced this line with next line
const [currentUser, setCurrentUser] = useState(() => isLoggedIn());
And I removed useeffect since I don't need it anymore.
// Setting current user
useEffect(() => {
setCurrentUser(isLoggedIn()).catch(err => console.log(err));
}, []);