Unexpected output using react-router-dom with React's Context API - javascript

For a small project of mine, I'm trying to implement the most basic authentication as possible, using the React context API without Redux.
import { createContext, useContext, useState } from 'react'
export const AuthContext = createContext()
export const useAuth = () => {
const context = useContext(AuthContext)
if(context === null) throw new Error('Nope')
return context
}
export const AuthProvider = (props) => {
const [authenticated, setAuthenticated] = useState(false)
const login = () => {
setAuthenticated(true)
localStorage.setItem(storageKey, true)
}
const logout = () => {
setAuthenticated(false)
localStorage.setItem(storageKey, false)
}
return <AuthContext.Provider value={{authenticated, login, logout}} {...props}/>
}
export default AuthContext
I created a context, and wrapped my <App /> component in it like so; <AuthProvider></App></AuthProvider>. Because I want to keep the authenticated state, I used the browser's local storage, for storing a simple boolean value.
import PrivateRoute from './PrivateRoute'
import { useAuth } from './context/AuthContext'
import { AuthPage } from './pages'
import {
BrowserRouter,
Switch,
Route,
} from 'react-router-dom'
import { useEffect } from 'react'
const App = () => {
const { login, authenticated } = useAuth()
useEffect(() => {
const token = localStorage.getItem('user')
if(token && token !== false) { login() }
})
return (
<BrowserRouter>
<Switch>
<PrivateRoute exact path="/auth" component={AuthPage} />
<Route exact path='/'>
Dashboard
</Route>
</Switch>
</BrowserRouter>
)
}
export default App
Then, in my <App /> component, I tried invoking the login callback, given from the AuthProvider, which made me assume that made me login during page refreshes. When I try to access the authenticated variable in the current component, it does work. It shows that I am authenticated.
However when I try to set up a PrivateRoute, which only authenticated users can go to like this:
import {
Route,
Redirect
} from 'react-router-dom'
import { useAuth } from './context/AuthContext'
const PrivateRoute = ({ component: Component, ...rest }) => {
const { authenticated } = useAuth()
if(authenticated) {
return <Route {...rest} render={(props) => <Component {...props} />} />
}
return <Redirect to={{ pathname: '/login' }} />
}
export default PrivateRoute
It does not work. It just redirects me to the login page. How does this come? The PrivateRoute component is getting rendered from the <App /> component. Also, what would be the solution to this problem?

Rather than running a useEffect on every rerender to check if user should be logged in, you should better initialize your authenticated state with the values from your localStorage:
const storageKey = 'user'
const initialState = JSON.parse(localStorage.getItem(storageKey)) ?? false
export const AuthProvider = (props) => {
const [authenticated, setAuthenticated] = useState(initialState)
const login = () => {
setAuthenticated(true)
localStorage.setItem(storageKey, true)
}
const logout = () => {
setAuthenticated(false)
localStorage.setItem(storageKey, false)
}
return <AuthContext.Provider value={{authenticated, login, logout}} {...props}/>
}

Thanks to Yousaf for the explaination in the comments and the HospitalRun project on GitHub, I made a loading state in the <App /> component.
import { useAuth } from './context/AuthContext'
import { useEffect, useState } from 'react'
import Router from './Router'
const App = () => {
const [ loading, setLoading ] = useState(true)
const { login } = useAuth()
const token = localStorage.getItem('user')
useEffect(() => {
if(token && token !== false) {
login()
}
setLoading(false)
}, [loading, token, login])
if (loading) return null
return <Router />
}
export default App
Here I only let anything render, after the login function was called.
if (loading) return null
If this could be done any better, feedback would still be appriciated!

Related

React Context - Reset itself to default value

I'm working on react context. I am creating a context to store the user info from the server, it is storing it fine but there is a wired situation here. Whenever I refresh the page, it reset itself to the default value which is in my case undefined and I am not sure why.
Here is the userContext
import React from "react";
const UserContext = React.createContext();
export default UserContext;
Here is the code for UserProvider
import { useEffect, useState, useContext, useMemo } from "react";
import UserContext from "./userContext";
import axios from "axios";
const UserProvider = (props) => {
const [user, setUser] = useState()
const userLogin = () => {
axios.defaults.headers.common = {'Authorization': `Bearer ${localStorage.getItem("token")}`}
axios
.get("/auth/profile")
.then((res)=>{
setUser(res.data)
})
}
const userLogout = () => {
setUser(null)
}
const providerValue = useMemo(() => ({user, userLogin, userLogout}), [user, userLogin, userLogout] )
return(
<UserContext.Provider value={providerValue}>
{ props.children }
</UserContext.Provider>
)
}
export default UserProvider
The purpose of the userLogin function is to get the info of the user. and the userLogout is to reset the user info to null.
Here is how I implemented it in the App.js
function App() {
const [token, setToken] = useState(localStorage.getItem("token"));
return (
<BrowserRouter>
<UserProvider>
<Routes>
<Route exact path="/" element={<Welcome />}></Route>
<Route path="/register" element={<Register />}></Route>
<Route path="/login" element={<Login />}></Route>
<Route path="/goal" element={<UserGoals />}></Route>
</Routes>
</UserProvider>
</BrowserRouter>
);
}
export default App;
I will not bother you with the details of the rest of the application but I will share with you how I use it in other components
const LoginForm = () => {
const {user, userLogin, userLogout} = useContext(UserContext)
}
Whenever I log in, I just simply call the function userLogin and whenever I want to access the user, I access it using user.
The problem occurs after call userLogin and then refreshing the page, the user becomes undefined. it occurs in multiple pages not only in the login page.
any help would be appreciated.
Most likely it's because you are not calling the userLogin function when the app first runs.
try adding this in UserProvider
useEffect(() => {
if (!user) {
userLogin()
}
}, [])

How can I force the user to go to Login before going to other component?

How can I make the admin go to login first before going to other components? So I've been getting an error about
TypeError: Cannot read properties of null (reading 'isAdmin')
I know where this came from, it's from my
const admin = useSelector((state) => state.user.currentUser.isAdmin)
I was thinking that forcing the admin to redirect to other page, but when I try to use ternary operator, the program itself already calls the useSelector.
Is there any way I can approach this problem?
import { useSelector } from 'react-redux'
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'
import './App.css'
// import Sidebar from './components/sidemenu/Sidebar'
import Home from './pages/home/Home'
import Login from './pages/login/Login'
// import NewProduct from './pages/newProduct/NewProduct'
// import NewUser from './pages/newUser/NewUser'
// import ProductList from './pages/product/ProductList'
// import Product from './pages/productItem/Product'
// import User from './pages/user/User'
// import UserList from './pages/userList/UserList'
function App() {
const admin = useSelector((state) => state.user.currentUser.isAdmin)
// const admin = false
return (
<Router>
<Switch>
<Route path="/">{admin ? <Home /> : <Login />}</Route>
</Switch>
</Router>
)
}
export default App
You should make your code safe.
For modern js you could use the optional chaining operator
const admin = useSelector((state) => state.user?.currentUser?.isAdmin)
A more traditional approach to do the same thing
const admin = useSelector((state) => state.user && state.user.currentUser && state.user.currentUser.isAdmin)
You can first check if currentUser exists.
If currentUser = null that means user is not authenticated and you can redirect them to Login
function App() {
const isLoggedIn = Boolean(useSelector((state) => state.user.currentUser))
const admin = isLoggedIn ? useSelector((state) => state.user.currentUser.isAdmin) : false
return (
<Router>
<Switch>
<Route path="/">{admin ? <Home /> : <Login />}</Route>
</Switch>
</Router>
)
}
you can use react hooks to handle authentication handling in all components.
for example you have context provider like this :
import React, { createContext, useContext, useEffect, useState } from 'react';
export const AuthContext = createContext();
const AuthProvider = ({ children }) => {
const [{ user, isLogin, isAdmin }, updateStatus] = useState({ isLogin: false, isAdmin: false });
// some functions to updates states
useEffect(() => {
if (isLogin && /* user is not in login page */) {
// redirect to login page
}
}, [isLogin])
if (!isLogin || !isAdmin)
return <></>
}
return (
<AuthContext.Provider value={{ user, isAdmin }}>{children}
</AuthContext.Provider>
}
(this context is very basic)
and you can wrap this provider in app.js
You can change
const admin = useSelector((state) => state.user.currentUser.isAdmin)
to
const admin = useSelector((state) => state.user?.currentUser?.isAdmin)
to fix this issue. It definitely works.

My Typescript React + Firebase App logs out on refresh

I am initializing a TS React App connected to Firebase with a private route when the user is logged in.
Everything seems to work well until I refresh the page. When I do that, the app takes me back to the public route which is a login page.
I think that the problem could be the initial state of the context which is set to null, but maybe it's a different problem.
Here is the code for my user context:
import React, { useContext, useState, useEffect } from "react";
import firebase from "firebase/app";
import { auth } from "../firebase";
export const AuthContext = React.createContext<firebase.User | null>(null);
export function useAuth() {
return useContext(AuthContext);
}
export const AuthProvider: React.FC = ({ children }) => {
const [user, setUser] = useState<firebase.User | null>(null);
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((firebaseUser) => {
setUser(firebaseUser);
});
return unsubscribe;
}, []);
return <AuthContext.Provider value={user}>{children}</AuthContext.Provider>;
};
Here is how I created the private route:
import React, { useContext } from "react";
import { Redirect, Route, RouteProps } from "react-router";
import { AuthContext } from "../contexts/AuthContext";
interface IPrivateRoute extends RouteProps {
component: any;
}
const PrivateRoute = ({ component: Component, ...rest }: IPrivateRoute) => {
const user = useContext(AuthContext);
setTimeout(() => console.log(user), 1000);
return (
<div>
<Route
{...rest}
render={(props) => {
return user ? <Component {...props} /> : <Redirect to="/login" />;
}}>
</Route>
</div>
);
};
export default PrivateRoute;
I will be grateful for all the helpful answers!
I think you need to save the session to stay authenticated. Whenever you refresh the page your user state will be null.
What worked for me is I used JWT. https://jwt.io/
After user logs in successfully my server sends the user a token and I save the token in user's cookies. For each PrivateRoute the user requests they will send the token back to server for verification. If the verification is successful then return the PrivateRoute to them.

Render route in React router dom after useEffect

I need to render the component after the useEffect checks if an user has the required role, but it always redirect me because it execute first the render function and then the useEffect
Here's my code:
import { Route, Redirect } from 'react-router-dom';
import React, { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { checkIfUserHasRequiredRole } from '../../utility/utility';
import PathConst from '../../utils/PathConst';
const AuthRoute = ({
Component, path, exact = false, isAuthenticated, requiredRoles,
}) => {
const [userHasRequiredRole, setUserHasRequiredRole] = useState(false);
const roles = useSelector((state) => state.role.roles);
const isAuthed = isAuthenticated;
useEffect(() => {
if (roles) {
const userRole = checkIfUserHasRequiredRole(requiredRoles, roles);
setUserHasRequiredRole(userRole);
}
}, [roles]);
return (
<Route
exact={exact}
path={path}
render={() => (
isAuthed && userHasRequiredRole ? (<Component />)
: (
<Redirect
to={PathConst.toLoginPage}
/>
))}
/>
);
};
export default AuthRoute;
The function 'checkIfUserHasRequiredRole' returns true but 'useHasRequiredRole' is still false when it trigger the if statement. I also tried using the same useEffect function with no dependencies.
How can I manage it?
You could do with your setup, but that requires additional render so that userHasRequiredRole is updated and take effect.
Since you can figure out what you need based on roles, you could,
import { useHistory } from "react-router-dom";
const history = useHistory();
useEffect(() => {
if (roles) {
const userRole = checkIfUserHasRequiredRole(requiredRoles, roles);
if (this role is not good) {
history.push(PathConst.toLoginPage)
}
}
}, [roles])

quick question about react context and rerendering components

I just started playing with context today and this is my usercontext
import { createContext, useEffect, useState } from "react";
import axios from "axios";
export const userContext = createContext({});
const UserContext = ({ children }) => {
const [user, setUser] = useState({});
useEffect(() => {
axios.get("/api/auth/user", { withCredentials: true }).then((res) => {
console.log(res);
setUser(res.data.user);
});
}, []);
return <userContext.Provider value={user}>{children}</userContext.Provider>;
};
export default UserContext;
this is how im using it in any component that needs the currently logged in user
const user = useContext(userContext)
my question is whenever the user logs in or logs out I have to refresh the page in order to see the change in the browser. is there any way that I can do this where there does not need to be a reload. also any general tips on react context are appreciated
(EDIT)
this is how Im using the UserContext if it helps at all
const App = () => {
return (
<BrowserRouter>
<UserContext>
<Switch>
{routes.map((route) => (
<Route
key={route.path}
path={route.path}
component={route.component}
/>
))}
</Switch>
</UserContext>
</BrowserRouter>
);
};
Where is your context consumer?
The way it is set up, any userContext.Consumer which has a UserContext as its ancestor will re render when the associated user is loaded, without the page needing to be reloaded.
To make it clearer you should rename your UserContext component to UserProvider and create a corresponding UserConsumer component:
import { createContext, useEffect, useState } from "react";
import axios from "axios";
export const userContext = createContext({});
const UserProvider = ({ children }) => {
const [user, setUser] = useState({});
useEffect(() => {
axios.get("/api/auth/user", { withCredentials: true }).then((res) => {
console.log(res);
// setting the state here will trigger a re render of this component
setUser(res.data.user);
});
}, []);
return <userContext.Provider value={user}>{children}</userContext.Provider>;
};
const UserConsumer = ({ children }) => {
return (
<userContext.Consumer>
{context => {
if (context === undefined) {
throw new Error('UserConsumer must be used within a UserProvider ')
}
// children is assumed to be a function, it must be used
// this way: context => render something with context (user)
return children(context)
}}
</userContext.Consumer>
);
};
export { UserProvider, UserConsumer };
Usage example:
import { UserConsumer } from 'the-file-containing-the-code-above';
export const SomeUiNeedingUserInfo = props => (
<UserConsumer>
{user => (
<ul>
<li>{user.firstName}</>
<li>{user.lastName}</>
</ul>
)}
</UserConsumer>
)
To be fair, you could also register to the context yourself, this way for a functional component:
const AnotherConsumer = props => {
const user = useContext(userContext);
return (....);
}
And this way for a class component:
class AnotherConsumer extends React.Component {
static contextType = userContext;
render() {
const user = this.context;
return (.....);
}
}
The benefit of the UserConsumer is reuasability without having to worry if you're in a functional or class component: it will used the same way.
Either way you have to "tell" react which component registers (should listen to) the userContext to have it refreshed on context change.
That's the whole point of context: allow for a small portion of the render tree to be affected and avoid prop drilling.

Categories