Private Route with Redux and react router - reactjs

I have a simple app with auth and private route, I want to get data from the server if the user has token, the back end is ready and works fine, and I log in I have data in redux about the user, but I don't know how to handle with refresh page, where should I do dispatch to call action? if I do it in privateRoute.js, it works strange I want to call server ones, but I did it 3-4 times.
Here are my components without calling sessionActions, sessionActions should update loggedIn and the user would go to /login pages
PrivateRoute
import React, { useState, useEffect } from 'react';
import { Route, Redirect } from 'react-router-dom';
import PropTypes from 'prop-types';
import sessionAction from '../store/actions/sessionAction';
const PrivateRoute = ({ component: Component, ...rest }) => {
const { path, dispatch, loggedIn } = rest;
});
return (
<Route
path={path}
render={(props) => (loggedIn ? <Component {...props} />
<Component {...props}/>
: (<Redirect to="/login" />))}
/>
);
};
PrivateRoute.propTypes = {
component: PropTypes.func.isRequired,
};
export default PrivateRoute;
sessionAction
const sessionAction = (path) => (dispatch) => {
return sessionServices(path)
.then((response) => {
console.log(response);
const { data } = response;
if (!data.error) {
dispatch(success(data));
}
dispatch(failure(data.error.text));
})
.catch((error) => error);
};
sessionService
import axios from 'axios';
axios.defaults.withCredentials = true;
const sessionServices = (path) => axios({
method: 'post',
url: `http://localhost:4000/api/pages${path}`,
})
.then((response) => response)
.catch((error) => console.log(error));
export default sessionServices;

You must dispatch the actions to fetch user data from the server in your App component which is the top-level component. Also, maintain a loading state in the reducer to render a Loader until the user data is fetched.
const App = props => {
useEffect(() {
this.props.sessionActions('/session')
}, []);
if(this.state.isLoading) return <Loader />;
const { loggedIn, user } = this.props;
return (
<Router>
{/* Your Private routes and other routes here */}
</Router>
)
}
const mapStateToProps = (state) => {
return {
isLoading: state.auth.isLoading,
user: state.auth.user,
loggedIn: state.auth.loggedIn
}
}
export default connect(mapStateToProps, { sessionAction })(App);

Related

User not being updated as I login using protected route in react route v6

I am trying to log a user in. I have a login component which takes the credentials and then I am using redux toolkit to store the state and the verification and everything is done in userSlice. I have a protected route which should check if the user is logged in or not and if the user is not logged in then it should not navigate to the recipe page which I have. when I try to access the user from the protected route component using useSelecter hook it returns empty on the first render but on the second render it does return user but the login still fails. in the redux dev tools the state is being updated just fine. is there a way where I can get the user object on the first render from the protected route component. (As you can see I am using the useEffect hook and have the dependency array too).
Any help would be greatly appreciated.
Thank you.
Below are my code:
Login.js -- this file is responsible for taking in the credentials and then dispatching an action and updating the state using useDispatch.
import React, { useState } from 'react';
import { loginUser } from '../../features/users/userSlice';
import { useSelector, useDispatch } from 'react-redux'
import { useNavigate } from 'react-router-dom';
export default function Login() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const user = useSelector(state => state.user.user)
const dispatch = useDispatch()
const navigate = useNavigate()
return (
<div>
<input
type="text"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button type="submit" onClick={() => {
dispatch(loginUser({email, password}))
navigate('/recipes')
}}>submit</button>
</div>
)
}
ProtectedRoute.js -- This component makes sure if the user is not authenticated he must not be able to login
import React, { useState, useEffect } from "react";
import { Route, Navigate, Outlet } from "react-router-dom";
import { useSelector } from 'react-redux';
export default function ProtectedRoute({ children }) {
const [ activeUser, setActiveUser ] = useState(false)
const user = useSelector((state) => state.user);
useEffect(() => {
if (!user.isLoading) {
user.success ? setActiveUser(true) : setActiveUser(false)
console.log('active user: ' + activeUser)
}
}, [user])
return (
activeUser ? <Outlet /> : <Navigate to="/login"/>
)
}
app.js -- This component has all the routes including the protected route.
import React from "react";
import Recipes from "./components/recipes/recipes";
import Login from "./components/users/Login";
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import ProtectedRoute from "./utils.js/ProtectedRoute";
const App = () => {
return (
<div>
<BrowserRouter>
<Routes>
<Route path="/login" index element={<Login />} />
<Route path="/" element={<Navigate replace to="/login" />}/>
<Route element={<ProtectedRoute />}>
<Route element={<Recipes/>} path="/recipes" />
</Route>
</Routes>
</BrowserRouter>
</div>
);
};
export default App;
userSlice.js -- since I am using redux toolkit hence I have slices for different things. This has reducers which have user state.
import { createSlice, createAsyncThunk } from "#reduxjs/toolkit";
import axios from "axios";
const loginUrl = 'http://localhost:5000/api/login';
const signupUrl = 'http://localhost:5000/api/signup';
export const loginUser = createAsyncThunk('user/loginUser', async (data) => {
const response = await axios.post(loginUrl, data);
return response;
})
export const signupUser = createAsyncThunk('user/signupUser', async (data) => {
const response = await axios.post(signupUrl, data);
return response;
})
const initialState = {
user: {},
isLoading: true
}
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
getPassword: (state, action) => {
const password = action.payload
console.log(password)
}
},
extraReducers: {
[loginUser.pending]: (state) => {
state.isLoading = false
},
[loginUser.fulfilled]: (state, action) => {
state.isLoading = false
state.user = action.payload.data
},
[loginUser.rejected]: (state) => {
state.isLoading = false
},
[signupUser.pending]: (state) => {
state.isLoading = false
},
[signupUser.fulfilled]: (state, action) => {
state.isLoading = false
state.user = action.payload.data
},
[signupUser.rejected]: (state) => {
state.isLoading = false
},
}
})
export const { getPassword } = userSlice.actions
export default userSlice.reducer;
The issue is that the login handler is issuing both actions "simultaneously".
onClick={() => {
dispatch(loginUser({ email, password })); // <-- log user in
navigate('/recipes'); // <-- immediately navigate
}}
The navigation action to the protected route occurs prior to the user being authenticated the redux state being updated.
To resolve the login handler should wait for successful authentication then redirect to the desired route.
const loginHandler = async () => {
try {
const response = await dispatch(loginUser({ email, password })).unwrap();
if (/* check response condition */) {
navigate("/recipes", { replace: true });
}
} catch (error) {
// handle any errors or rejected Promises
}
};
...
onClick={loginHandler}
See Handling Thunk Results for more details.
You may also want to simplify the ProtectedRoute logic to not require an extra rerender just to get the correct output to render. All the route protection state can be derived from selected redux state.
export default function ProtectedRoute() {
const user = useSelector((state) => state.user);
if (user.isLoading) {
return null; // or loading indicator/spinner/etc
}
return user.success ? <Outlet /> : <Navigate to="/login" replace />;
}

Using result of useSelector() in component

I am using react-redux to manage state and axios to make API calls.
My default state:
const defaultState = {
isLoading: true,
isAuthenticated: false,
authUser: {}
}
isLoading and authUser are both updated once the API's login call has ran successfully. Nothing wrong here.
In a userProfile component, I am using the following to get the logged in user's ID from the store:
const authUser = useSelector(state => state.authentication.authUser);
const user = users.getUser(authUser.id);
console.log(authUser);
authUser is always empty on page load but a split second late, it prints again, this time with the state contents.
I have the userProfile component behind a custom PrivateRoute component:
const PrivateRoute = ({ component: Component, ...rest }) => {
const authentication = useSelector(state => state.authentication);
return (
<Route
{...rest}
render={props =>
authentication.isAuthenticated ? (
<Component {...props} />
) : (
authentication.isLoading ? 'loading...' :
<Redirect to={{ pathname: '/login', state: { from: props.location } }} />
)
}
/>
)
};
The loading... text shows before the component catches up, then the 2 console logs appear one after the other. The first empty, the second with data.
What am I missing here? This async stuff is driving me insane!
with this implementation of private route u can save current path of user and let user continue his process after login again
import React from 'react';
import { Login } from '../../pageComponents';
import { useSelector } from 'react-redux';
const PrivateRoute = (Component) => {
const Auth = (props) => {
const { isLoggedIn } = useSelector((state) => state.user);
// Login data added to props via redux-store (or use react context for example)
// If user is not logged in, return login componentc
if (!isLoggedIn) {
return <Login />;
}
// If user is logged in, return original component
return <Component {...props} />;
};
// Copy getInitial props so it will run as well
if (Component.getInitialProps) {
Auth.getInitialProps = Component.getInitialProps;
}
return Auth;
};
export default PrivateRoute;
then user it every page you want Private Rout like below
const myPrivateRoute = () => {
return (
<h1>Hello world</h1>
)
}
export default PrivateRoute(myPrivateRoute)
so create component like this and load your last section from local storage or cookie then validate and send into your redux store then you can memorize log
import { useEffect, useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import axios from "axios";
import * as globalActions from "../../../store/actions/globalAction";
import * as userAction from "../../../store/actions/userAction";
const StorageManagment = () => {
const dispatch = useDispatch();
/* ----------------------------------- listening to token changes ----------------------------------- */
useEffect(() => {
dispatch(userAction.loadCart());
}, []);
useEffect(async () => {
try {
if (window)
await checkLocalDataWithRedux()
.then((data) => {
const { userDTO, token } = data;
dispatch(userAction.loginUser(token, userDTO));
})
.catch((e) => {
console.log(e);
dispatch(userAction.logOutUser());
});
} catch (e) {
throw e;
}
}, []);
function checkLocalDataWithRedux() {
return new Promise((resolve, reject) => {
try {
/* -------- read user data from localStorage and set into redux store ------- */
const { userDTO, token } = localStorage;
if (userDTO && token) {
resolve({ token: token, userDTO: JSON.parse(userDTO) });
} else logOutUser();
} catch (e) {
reject();
}
});
}
function logOutUser() {
dispatch(userAction.logOutUser());
}
return null;
};
export default StorageManagment;
then load it in your app.jsx file
app.js
render() {
return (
<React.Fragment>
<Provider store={store}>
<ErrorBoundary>
<StorageManagment />
<DefaultLayout>
<Application {...this.props} />
</DefaultLayout>
</ErrorBoundary>
</Provider>
</React.Fragment>
);
}

Check user on every page using react and redux

In my App.tsx file I have this setup
const App: React.FC = props => {
const [hasRendered, setHasRendered] = useState(false);
const dispatch = useDispatch();
const isAuth = authMiddleWare();
useEffect(() => setHasRendered(true), [hasRendered]);
if (!hasRendered && isAuth !== null) {
if (isAuth) {
dispatch(getUser());
} else {
dispatch(logoutUser());
}
}
...
<PrivateRoute path="/app" component={Dashboard} />
}
const PrivateRoute = ({ component, location, ...rest }: any) => {
const dispatch = useDispatch();
const authenticated = useSelector((state: RootState) => state.user.authenticated);
return <>{authenticated ? React.createElement(component, props) : <Redirect to={{ pathname: LoginRoute, state: { from: props.location } }} </>;
};
However, authentication only gets executed on browser reload, so data is not updated if I click a link within my app. I want the user to be checked when loading any private route.
Create a separate auth component and call it in every route and check the user is authenticated or not.
if the user authenticated then requested route show else return the 404page or homepage.
or check in every page you can check your authentication by putting this code in componentditmount section
import React, { Component } from 'react'
import { connect } from "react-redux";
import { userLoginAction } from "./store/actions/userLogin";
import * as actionType from "./store/actions/actionType";
import { Redirect } from 'react-router-dom';
class componentName extends Component {
constructor(props){
super(props)
this.state={
isMount:false
}
}
componentDidMount(){
const token = localStorage.getItem("token");
if (token != null && this.props.loginStatus === false) {
// this.userLoginData(token)
this.props.auth();
}
this.setState({ isMount: true });
}
render() {
return (
<>
{this.state.isMount?this.props.auth===false?<Redirect from ='/' to='/login' />:
<p>Hello World!</p> :<div>Loging.....</div>
}
</>
)
}
}
const mapGetState = (state) => {
return {
loginStatus: state.usrReducer.login_st,
auth: state.usrReducer.auth,
};
};
const mapDispatchState = (dispatch) => {
return {
login: (data) => dispatch({ type: actionType.LOGIN_ST, payload: data }),
auth: (data) => dispatch(userLoginAction(data)),
};
};
export default connect(mapGetState, mapDispatchState)(componentName)

lost auth state when page refresh in reactjs hooks

When i login with valid credentials server send JWT to browser i store this JWT in localstore page redirect to home page everything works fine and in home page i have loadUser function which send request to server to get the user details of valid user but when i refresh the page it home page never execute because auth state returns to false and page redirect from home to login page
This is App.js
import React, { Fragment } from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import Navbar from "./components/layout/Navbar";
import Home from "./components/pages/Home";
import Login from "./components/auth/Login";
import PrivateRoute from "./components/routing/PrivateRoute";
import "./App.css";
const App = () => {
return (
<AuthState>
<Router>
<Fragment>
<Navbar />
<div className="container">
<Alerts />
<Switch>
<PrivateRoute exact path="/" component={Home} />
<Route exact path="/login" component={Login} />
</Switch>
</div>
</Fragment>
</Router>
</AuthState>
);
};
export default App;
This is my authsate code from where my state changes after successful login
import React, { useReducer } from "react";
import axios from "axios";
import AuthContext from "./authContext";
import authReducer from "./authReducer";
import {
USER_LOADED,
AUTH_ERROR,
LOGIN_SUCCESS,
LOGIN_FAIL,
} from "../types";
const AuthState = props => {
const initialState = {
isAuthenticated: null,
user: null,
};
const [state, dispatch] = useReducer(authReducer, initialState);
// Load User
const loadUser = async () => {
if (localStorage.token) {
setAuthToken(localStorage.token);
}
try {
const res = await axios.get("/api/auth");
dispatch({ type: USER_LOADED, payload: res.data });
} catch (err) {
dispatch({ type: AUTH_ERROR });
}
};
// Login User
const login = async formData => {
const config = {
headers: {
"Content-Type": "application/json"
}
};
try {
const res = await axios.post("api/auth", formData, config);
dispatch({
type: LOGIN_SUCCESS,
payload: res.data
});
loadUser();
} catch (err) {
dispatch({
type: LOGIN_FAIL,
payload: err.response.data.msg
});
}
};
return (
<AuthContext.Provider
value={{
isAuthenticated: state.isAuthenticated,
user: state.user,
loadUser,
login,
}}
>
{props.children}
</AuthContext.Provider>
);
};
export default AuthState;
This is reducer code authReducer.js
import {
USER_LOADED,
AUTH_ERROR,
LOGIN_SUCCESS,
LOGIN_FAIL,
} from "../types";
export default (state, action) => {
switch (action.type) {
case USER_LOADED:
return {
...state,
isAuthenticated: true,
user: action.payload
};
case LOGIN_SUCCESS:
localStorage.setItem("token", action.payload.token);
return {
...state,
isAuthenticated: true
};
case AUTH_ERROR:
case LOGIN_FAIL:
localStorage.removeItem("token");
return {
...state,
isAuthenticated: false,
user: null,
};
default:
return state;
}
};
This is private route
import React, { useContext } from "react";
import { Route, Redirect } from "react-router-dom";
import AuthContext from "../../context/auth/authContext";
const PrivateRoute = ({ component: Component, ...rest }) => {
const authContext = useContext(AuthContext);
const { isAuthenticated, loading } = authContext;
return (
<Route
{...rest}
render={props =>
!isAuthenticated && !loading ? (
<Redirect to="/login" />
) : (
<Component {...props} />
)
}
/>
);
};
export default PrivateRoute;
That's to be expected.
If you want to persist the token you should save it in localStorage or a cookie.
Then, you should check for the token existence and validity in componentDidMount or in an useEffect (with a void dependency array) if you are using hooks.
It'd be nice if you could show us where you are making AJAX requests because if, for example, you are using Axios you can encapsulate this logic in a request interceptor.
Probably the JSON web token which is used for user authentication/authorization. Can you give us more information about this?

How to refresh JWT token when App launches

I want to login user automatically if refresh_token exists in localStorage but my issue here is, i am not able to change state to authenticated initially when app start. I tried with componentWillMount but i am getting old state 1st time then i get updated state. <PrivateRoute> here getting called 2-3 time, don't know where i am making mistake.
Expected Flow
autoLogin
PrivateRoute with new state
App.js
import React, { Component } from 'react'
import WrappedLoginForm from './containers/Login'
import ChatApp from './containers/Chat'
import { connect } from 'react-redux'
import { checkAuthentication } from './store/actions/auth'
import PrivateRoute from './route'
import {
BrowserRouter as Router,
Route
} from "react-router-dom";
class App extends Component {
componentWillMount() {
this.props.autoLogin()
}
render() {
return (
<Router>
<Route path='/login' component={WrappedLoginForm} />
<PrivateRoute path="/" component={ChatApp} authed={this.props.isAuthenticated} />
</Router>
)
}
}
const mapStateToProps = state => {
return {
isAuthenticated: state.isAuthenticated,
loading: state.loading
}
}
const mapDispatchToProps = dispatch => {
return {
autoLogin: () => {
dispatch(checkAuthentication())
}
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(App)
route.js
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
const PrivateRoute = ({ component: Component, authed, ...rest }) => {
console.log('aaa')
return (<Route
render={props => (
authed
? <Component />
: <Redirect to="/login" />
)}
/>)
};
export default PrivateRoute;
action.js
export const checkAuthentication = () => {
return dispatch => {
dispatch(authStart())
let refresh_token = localStorage.getItem('refresh_token')
axiosInstance.post('/api/token/refresh/', {
refresh: refresh_token
}).then(res => {
if (res.status === 200) {
localStorage.setItem("access_token", res.data.access)
dispatch(loginSuccess())
}
}).catch(error => {
console.log("balle")
dispatch(loginFailed(error))
})
}
}
first thing make sure that in your store the isAuthenticated property is false by default.
For PrivateRoute use :
{this.props.isAuthenticated
? <PrivateRoute path="/" component={ChatApp} />
: null
}
instead of <PrivateRoute path="/" component={ChatApp} authed={this.props.isAuthenticated} />

Resources