I'm relatively new to React and Redux and learning them through my personal project.
The issue here is that isAuthed cannot use the updated Redux state after rest.dispatch(actions.isValidUser(json)) is executed. As far as I know, the Redux state is updated by the action. (But I don't see connect() is called after the update...I don't know if this is associated with this problem.)
Also I tried using Redux-thunk in my action file to fetch data from an API endpoint and using useEffect(), but it didn't solve the issue. Could you please help me out?
Thank you in advance.
**ProtedtedRoute.jsx**
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
import { connect } from 'react-redux';
import * as actions from '../actions/actions';
function ProtectedRoute({ component: Component, isAuthed, ...rest }) {
async function verifyUser() {
// checking if a user is valid by checking JWT
const res = await fetch(ENDPOINT, reqOptions);
if (res.status === 200) {
const json = await res.json();
rest.dispatch(actions.isValidUser(json));
} else {
// failure handling
};
};
verifyUser();
return (
<Route
{...rest}
render={(props) => isAuthed == true ? <Component {...props} /> : <Redirect to={{ pathname: '/login', state: { from: props.location } }} />}
/>
);
};
export default connect(state => {
return {
isAuthed: state.isAuthenticated
}
})(ProtectedRoute);
**reducer.js**
const initState = {
data: {},
// when a user is valid, it will be ```true```
isAuthenticated: false
}
**App.js**
function App() {
return (
<Provider store={store}>
<BrowserRouter>
<div>
<div className="content">
<Switch>
<Route exact path="/" component={Home} />
<PublicRoute path="/login" component={LogIn} />
<PublicRoute path="/signup" component={SignUp} />
<ProtectedRoute path="/dashboard" component={Dashboard} />
</Switch>
...
**Login.jsx**
const res = await fetch(ENDPOINT, { reqOptions});
if (res.status === 200) {
props.history.push('/dashboard');
else{
// error handling
}
You don't want a function call like verifyUser(); just floating in the component. It needs to be inside a useEffect hook.
Your Login component fetches the endpoint before you redirect to Dashboard, so you should not need to fetch the endpoint again in order to access the Dashboard through PrivateRoute.
You can change your initialState to include isAuthenticated: undefined as in "we don't know if they are authenticate or not because we haven't checked yet."
Then in PrivateRoute, we only need to call verifyUser if the value of isAuthed is undefined meaning that we haven't checked yet. If it's true or false we just use that existing value.
We still have a bit of a problem with the aysnc flow because we don't want to to Redirect off of the PrivateRoute before verifyUser has finished. For that, we can conditionally render a loading state that shows while awaiting credentials.
I don't know that this is the most elegant solution but it should work
function ProtectedRoute({ component: Component, isAuthed, ...rest }) {
async function verifyUser() {
// checking if a user is valid by checking JWT
const res = await fetch(ENDPOINT, reqOptions);
if (res.status === 200) {
const json = await res.json();
rest.dispatch(actions.isValidUser(json));
} else {
// failure handling
}
}
useEffect(() => {
if (isAuthed === undefined) {
verifyUser();
}
}, [isAuthed]); //re-run when isAuthed changes
return (
<Route
{...rest}
render={(props) =>
isAuthed === undefined ? (
<Loading />
) : isAuthed === true ? (
<Component {...props} />
) : (
<Redirect
to={{ pathname: "/login", state: { from: props.location } }}
/>
)
}
/>
);
}
Related
here is my protectedroute component
am using react-router-dom v6 and accessing the token from localStorage
and either ways user is always returning undefined
import { Outlet, Navigate} from "react-router-dom";
import axios from "axios";
const ProtectedRoute = () => {
const userAuth = () => {
axios.get("http://localhost:5000/isUserAuth", {
headers: {
"x-access-token": localStorage.getItem("token")
}}).then((response) => {
console.log(response.data)
if(response.data.auth) {
console.log(true)
return true;
} else {
console.log(false)
return false;
}
})
}
let auth = userAuth()
console.log("auth",auth)
return (
auth? <Outlet/> : <Navigate to="/"/>
)
}
export default ProtectedRoute
my app.js
function App() {
return (
<BrowserRouter>
<ToastContainer position='top-center'/>
<Routes>
<Route element={<ProtectedRoutes/>}>
<Route exact path='/home'
element={< Home />}/>
<Route exact path='/add'
element={< AddCust />} />
<Route exact path='/update/:id'
element={< AddCust />} />
<Route exact path='/view/:id'
element={< View />} />
<Route exact path='/table'
element={< Table />} />
<Route exact path='/edit-order/:id'
element={< Table />} />
<Route exact path='/orders'
element={< Orders />} />
</Route>
<Route exact path='/' element={< Login />} />
</Routes>
</BrowserRouter>
);
}
export default App;
this is what is consoled logged
enter image description here
which is weired whether a token exists or not auth is always undefined
Nothing is actually returned from the userAuth function, so auth is undefined. While you could return the axios Promise object, this will make userAuth an asynchronous function and not usable as a ternary condition to conditionally render the Outlet component or redirect.
A solution then is to convert auth to a React state, updated in the GET request flow, and conditionally render null or a loading indicator until the auth status resolves.
Example:
const ProtectedRoute = () => {
const { pathname } = useLocation();
const [auth, setAuth] = React.useState(); // initially undefined
React.useEffect(() => {
const checkAuth = async () => {
try {
const response = await axios.get(
"http://localhost:5000/isUserAuth",
{
headers: {
"x-access-token": localStorage.getItem("token")
},
}
);
setAuth(!!response.data.auth);
} catch(error) {
// handle error, log, etc...
setAuth(false);
}
};
checkAuth();
}, [pathname]); // trigger auth check on route change
if (auth === undefined) {
return null; // loading indicator/spinner/etc
}
return auth
? <Outlet/>
: <Navigate to="/" replace state={{ from: pathname }} />;
};
I have protected routes that passing auth enabled true or false to allow users access auth page. Every page load, it calls the auth token API and check for valid. If token invalid, redirect to login page.
ProtectedRoute.js
const ProtectedRoute = ({isEnabled, ...props}) => {
return (isEnabled) ? <Route {...props} /> : <Redirect to="/login"/>;
};
export default ProtectedRoute;
Routes.js
import {withRouter, Switch, Route } from "react-router-dom";
export default withRouter(({ location }) => {
const [isAuth, setIsAuth] = useState(false)
useLayoutEffect(() => {
(async() => {
if(accessToken){
let res = await ValidateLoginToken(accessToken)
if (res && res.data && res.data.status === 200){
setIsAuth(res.data.valid)
} else setIsAuth(false);
} else setIsAuth(false)
})()
},[isAuth])
return (
<Switch>
<ProtectedRoute path="/dashboard" component={Dashboard} isEnabled={isAuth} />
<Route path="/" component={Login} />
</Switch>
)
}
App.js
const history = createBrowserHistory();
function App() {
return (
<Router history={history}>
<Routes/>
</Router>
)
}
export default App;
Dashboard.js
export const Dashboard = () => {
return (
<div class="ui form-user center raised padded segment">
<a href="/section1">
<div id="section1" class="ui floated right basic red button">
Directory #1
</div>
</a>
</div>
)
}
Problem
When auth users browse authenticated page (Dashboard), redirects to login page. The reason is that verify token API returns after ProtectedRoute rendered as isAuth equals to false.
You can use another state variable to wait for api execution before initializing routes.
const [isAuth, setIsAuth] = useState(false)
const [checked, setChecked] = useState(false)
useLayoutEffect(() => {
(async() => {
if(accessToken){
let res = await ValidateLoginToken(accessToken)
if (res && res.data && res.data.status === 200){
setIsAuth(res.data.valid)
} else setIsAuth(false);
} else setIsAuth(false)
setChecked(true)
})()
},[isAuth])
Then in Routes, you can do something like:
<Switch>
{
!checked?(<React.Fragment/>):!isAuth?(<Route path="/" component={Login} />):(<ProtectedRoute path="/dashboard" component={Dashboard} loginState={loginState} />)
}
</Switch>
I usually separate session routes and no session routes in separate hooks and it works fine.
Instead of isAuth being a boolean, you use a string or number to increase the number of states. For example rename isAuth to loginState which can be "pending", "authenticated" or "unauthenticated". Then use "pending" as the initial state and add an additional scenario. You could for example return null to render nothing, render a spinning circle, etc.
Here is an example rendering null (nothing) while the login token is being authenticated:
ProtectedRoute.js
const loginRoutes = {
pending: ( ) => null,
authenticated: (...props) => <Route {...props} />,
unauthenticated: ( ) => <Redirect to="/login"/>,
};
const ProtectedRoute = ({loginState = "pending", ...props}) => {
const LoginRoute = loginRoutes[loginState];
return <LoginRoute {...props} />;
};
export default ProtectedRoute;
Route.js
import { withRouter, Switch, Route } from "react-router-dom";
export default withRouter(({ location }) => {
const [loginState, setLoginState] = useState("pending")
useLayoutEffect(() => {
(async() => {
if(accessToken){
let res = await ValidateLoginToken(accessToken)
if (res && res.data && res.data.status === 200) {
setLoginState(res.data.valid ? "authenticate" : "unauthenticated")
} else setLoginState("unauthenticated");
} else setLoginState("unauthenticated")
})()
}, [loginState])
return (
<Switch>
<ProtectedRoute path="/dashboard" component={Dashboard} loginState={loginState} />
<Route path="/" component={Login} />
</Switch>
)
}
As a disclaimer, I have no experience with React Router so I've kept the example as close to the original as possible.
You could use the initial state as null to differentiate the different states of your component.
null -> API token no called yet.
true -> token verified
false -> token verification failed.
export default function ProtectedRoute(props) {
const [isAuth, setIsAuth] = React.useState(false)
const history = useHistory();
React.useEffect(() => {
async function validateToken() {
if(accessToken){
let res = await ValidateLoginToken(accessToken)
if (res && res.data && res.data.status === 200){
setIsAuth(true)
return;
}
}
history.push('/login')
}
validateToken();
}, [isAuth]);
if (isAuth === null) return null;
return props.children;
}
export default function App() {
return (
<Router>
<Switch>
<Route path="/" exact component={HomePage} />
<Route exact path="/login" component={Login} />
<ProtectedRoute>
<Route path="/dashboard" component={Dashboard} />
</ProtectedRoute>
</Switch>
</Router>
)
}
I have a react + firebase application which has protected routes. I face issue if a logged in user accesses the login page. The issue is that the login page gets displayed for a second and then redirects to the home page i.e protected route. I feel the issue is because the value retrieved from context in the login page to check if user is authenticated gets updated after the route is resolved. Can someone give me pointers on how should I fix this. Ideally I would not want the user to see the login page for sometime if the user is already authenticated.
//App.js
render() {
return (
<AuthProvider>
<Router>
<Switch>
<PrivateRoute exact path="/" component={Home}></PrivateRoute>
<Route exact path="/login" component={LoginPage}></Route>
<Route exact path="/signup" component={SignUp}></Route>
</Switch>
</Router>
</AuthProvider>
);
}
}
//AuthProvider
import React, { useEffect, useState } from "react"
import { fire } from "./Fire"
export const AuthContext = React.createContext();
//this component will maintain the current user throughout the app
export const AuthProvider = ({ children }) => {
const [currentUser, setCurrentUser] = useState(null)
//empty array as second arg to useEffect hook as we only want to trigger it once
useEffect(() => {
console.log("useEffect")
fire.auth().onAuthStateChanged(setCurrentUser)
}, [])
return (
<AuthContext.Provider value={{ currentUser }}>{children}</AuthContext.Provider>
)
}
//PrivateRoute
const PrivateRoute = ({ component: RouteComponent, ...rest }) => {
//useContext hook makes it very easy to retrieve the value
const { currentUser } = useContext(AuthContext)
return (
<Route {...rest} render={
routeProps => {
console.log("currentUser" + currentUser);
return !!currentUser ? (
<RouteComponent {...routeProps} />
) : (
<Redirect to={"/login"} />
)
}
} />
)
}
//login
render() {
if (this.props.context.currentUser)
return <Redirect to="/" />
return (
<Login email={this.state.email} password={this.state.password} inputHandler={this.onInputChange} loginHandler={this.onLoginClick} />
)
}
You should probably add a loading check in your PrivateRoute file which will just show a Loading... or a loader if the user is not loaded yet.
For that you will have to do some minor changes in your AuthProvider file and in PrivateRoute.
//PrivateRoute
const PrivateRoute = ({ component: RouteComponent, ...rest }) => {
//useContext hook makes it very easy to retrieve the value
const { currentUser } = useContext(AuthContext)
return (
<Route {...rest} render={
routeProps => {
console.log("currentUser" + currentUser);
return !!currentUser ? (
<RouteComponent {...routeProps} />
) : currentUser === 'loading' ? <h1>Loading...</h1>
:(
<Redirect to={"/login"} />
)
}
} />
)
}
So I am creating a web app with server (node+express) and client (cra) sides.
I have an issue with validating user according to the jwt token I set as a cookie. The validation api endpoint on server side works as it should (tested it with postman) but the problem is the async check function that returns the promise therefore the route doesn't really know is it validated since the response is pending.
Here is the api endpoint on server side:
/api/token.js
router.get('/',
jwt({secret:'token-secret' }),
function (req,res) {
console.log(req);
if(!req.user) return res.sendStatus(401);
res.sendStatus(200);
}
)
and here is the app.js on the client side: src/app.js that handles routing ( /dashboard should be available only for validated users)
function App() {
function checkToken() {
let token = Cookies.get('Access Token')
axios.get('http://localhost:9000/api/token', {
headers: {
'Authorization': `bearer ${token}`
}
}).then(res => {
return res.status;
}).catch(err => console.log(err));
}
const handleToken = async () => {
const result = await checkToken();
return result;
}
return (
<BrowserRouter>
<Route exact={true} path='/' render={() => (
<div className="App">
<Home />
</div>
)}/>
<Route exact={true} path='/dashboard' render={() => (
<div className="App">
{console.log('checktoken log', handleToken())}
{checkToken() ? <Dashboard /> : <Login />}
</div>
)}/>
<Route exact={true} path='/login' render={() => (
<div className="App">
<Login />
</div>
)}/>
</BrowserRouter>
);
}
At this point I am aware that perhaps I shouldn't be doing a validation in this way since there is probably no way I can get the return before the render, perhaps it should be done in a lifecycle hook componentWillMount but I haven't been able to introduce it to this file (or everything should be done in an entirely different file).
Thanks
p.s. I've omitted all imports and export defaults since that is not relevant here
Well, I've made it with a couple of substantial changes. First, in order to use history.push I had to refactor BrowserRouter part so now it looks like this
app.js
render() {
return (
<Router history={history}>
<Route exact path='/' component={Home} />
<Route exact path='/dashboard' component={Dashboard} />
<Route exact path='/login' component={Login} />
</Router>
);
}
Then I've decided not to use api/token.js. Instead of this api endpoint I've created a Higher Order Component that will check for cookies that have been set during login. Part that gave me most trouble is asynchronous nature of fetching cookies. That was solved with setTimeout inside getCookie function, I've called this function in componentDidMount lifecycle.
src/components/withAuth.js
state = {
data: false,
open: false,
auth: false
}
componentDidMount() {
this.getCookie();
}
getCookie(){
this.setState({
open: true,
})
setTimeout(() => {
const cookie = Cookies.get('Access Token')
if(cookie) {
this.setState({
data: true,
open: false,
auth: true
})
} else if (cookie === undefined) {
this.setState({
auth: true,
open: false
})
}
}, 700)
}
In the end, in order to protect the route I've wrapped the component with HOC
src/Views/Dashboard.js
import requireAuthentication from '../components/withAuth';
class Dashboard extends Component {
render() {
return (
<div>
<DashboardContent />
</div>
);
}
}
export default requireAuthentication(Dashboard);
I'm developing a basic react application and included react-router.
I have a simple authentication control with the Local Storage.
After a user inputs username and password and clicks login, I do an HTTP call and take response from the server with Axios. Then I set the localStorage 'user' item.
To protect a route I implemented the PrivateRoute component where I check if 'user' in localStorage is set.
I already tried to move set Local Storage inside then() in the Axios HTTP call but nothing changed.
Api CALL
loginUser (username,password) {
return HTTP.post('/login', null, { params: {
username,
password
}})
Api.loginUser(username,password)
.then( (response) => {
console.log("Response DATA");
Api.saveUserData(response.data);
this.setState({ redirect: true });
})
RENDER METHOD
if (this.state.redirect === true) {
return <Redirect to='/home'/>;
}
APP COMPONENT
class App extends Component {
render() {
return (
<Router>
<Route path="/login" component={Login} />
<PrivateRoute path="/home" component={Home} />
</Router>
);
}
}
PRIVATE ROUTE COMPONENT
const PrivateRoute = ({ component: Component, ...rest }) => {
const isLoggedIn = AuthService.isAuthenticated();
return (
<Route
{...rest}
render={props =>
isLoggedIn ? (
<Component {...props} />
) : (
<Redirect to={{ pathname: '/login', state: { from: props.location } }} />
)
}
/>
)
}
The problems seem to be: the local storage is set after the redirect because is null. So I get blank page instead of loading the Home Component. If i refresh the page, the code works fine.