PrivateRoute.js
import { useEffect } from 'react';
import { Navigate } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import Spinner from '../layout/Spinner';
const PrivateRoute = ({ children }) => {
const { loading, isAuthenticated } = useSelector((state) => state.auth);
if (loading) return <Spinner />
if (!isAuthenticated) return <Navigate to='/login' />
return children;
}
export default PrivateRoute;
App.js
import './App.css';
import 'react-phone-number-input/style.css'
import { useEffect } from 'react';
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import { loadUser } from './reducers/authSlice';
import setAuthToken from './utils/setAuthToken';
import Products from './screens/Products';
import Product from './screens/Product';
import Navigator from './components/layout/Navigator';
import Footer from './components/layout/Footer';
import Signup from './screens/Signup';
import Login from './screens/Login';
import Dashboard from './screens/Dashboard';
import Settings from './screens/Settings';
import PrivateRoute from './components/routing/PrivateRoute';
import Store from './screens/Store';
import CreateOrUpdateStore from './screens/CreateOrUpdateStore';
import AddProduct from './screens/AddProduct';
import UpdateProduct from './screens/UpdateProduct';
const token = localStorage.getItem("token");
if (token) {
setAuthToken(token);
}
function App() {
const dispatch = useDispatch();
const { isAuthenticated } = useSelector((state) => state.auth);
useEffect(() => {
if (!isAuthenticated && token) {
dispatch(loadUser());
}
}, []);
return (
<Router>
<Navigator />
<div className="container">
<Routes>
<Route path="/" element={<Products />} />
<Route path="/products/:id" element={<Product />} />
<Route path="/signup" element={<Signup />} />
<Route path="/login" element={<Login />} />
<Route path="/dashboard" element={<PrivateRoute><Dashboard /></PrivateRoute>} />
<Route path="/settings" element={<PrivateRoute><Settings /></PrivateRoute>} />
<Route path="/store" element={<PrivateRoute><Store /></PrivateRoute>} />
<Route path="/create-store" element={<PrivateRoute><CreateOrUpdateStore /></PrivateRoute>} />
<Route path="/update-store" element={<PrivateRoute><CreateOrUpdateStore /></PrivateRoute>} />
<Route path="/add-product" element={<PrivateRoute ><AddProduct /></PrivateRoute>} />
<Route path="/update-product/:id" element={<PrivateRoute><UpdateProduct /></PrivateRoute>} />
</Routes>
</div>
<Footer />
</Router>
)
}
export default App;
Login.js
import { useState, useEffect } from 'react';
import { Navigate } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux';
import InputField from '../components/layout/InputField';
import Button from '../components/layout/Button';
import { faEnvelope, faUserCircle, faLock, faArrowRightToBracket } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { loginUser, clearMessages } from '../reducers/authSlice';
import FormAlert from '../components/layout/FormAlert';
import Spinner from '../components/layout/Spinner';
const Login = () => {
const [ formData, setFormData ] = useState({
email:'',
password:''
});
const { email, password } = formData;
const dispatch = useDispatch();
const { errors, message, loading, isAuthenticated } = useSelector((state) => state.auth);
useEffect(() => {
dispatch(clearMessages());
},[dispatch]);
const onChange = (e) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
const getError = (name) => {
const findError = errors.filter(error => error.param === name);
if (findError.length > 0) {
const error = errors.find(error => error.param === name);
return error;
}
}
const onSubmit = (e) => {
e.preventDefault();
const data = { email, password };
dispatch(loginUser(data));
setTimeout(() => {
dispatch(clearMessages());
},3000);
}
if (loading) return <Spinner />
if (isAuthenticated) return <Navigate to="/dashboard" />
return (
<form onSubmit={(e) => onSubmit(e)}>
<FontAwesomeIcon style={{ backgroundColor: 'inherit', color: '#2596be', marginTop: '20px' }} size='8x' icon={faUserCircle} />
{JSON.stringify(message) !== '{}' ? (<FormAlert alert={message} />) : ''}
<InputField type='text' label='Email' name='email' value={email} changeHandler={onChange} error={getError('email')} icon={faEnvelope} />
<InputField type='password' label='Password' name='password' value={password} changeHandler={onChange} error={getError('password')} icon={faLock} />
<Button text='LOGIN' loading={loading} icon={faArrowRightToBracket} />
</form>
);
}
export default Login;
authSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { authAPI } from '../services/authAPI';
// Function for logging in user
export const loginUser = createAsyncThunk(
"auth/loginUserStatus",
async (data, { rejectWithValue }) => {
try {
const response = await authAPI.loginUser(data);
return response.data;
} catch (error) {
return rejectWithValue(error.response.data);
}
}
);
// Function for loading user data
export const loadUser = createAsyncThunk(
"auth/loadUserStatus",
async (_, { rejectWithValue }) => {
try {
const response = await authAPI.loadUser();
return response.data;
} catch (error) {
return rejectWithValue(error.response.data);
}
}
);
// Function for signing up a user
export const signupUser = createAsyncThunk(
"auth/signupUserStatus",
async (data, { rejectWithValue }) => {
try {
const response = await authAPI.signupUser(data);
return response.data;
} catch (error) {
return rejectWithValue(error.response.data);
}
}
);
// Function for verifying user accounts after sign up
export const verifyUser = createAsyncThunk(
"auth/verifyUserStatus",
async (data, { rejectWithValue }) => {
try {
const response = await authAPI.verifyUser(data);
return response.data;
} catch (error) {
return rejectWithValue(error.response.data);
}
}
);
// Function for resending otp
export const resendOtp = createAsyncThunk(
"auth/resendOtpStatus",
async (data, { rejectWithValue }) => {
try {
const response = await authAPI.resendOtp(data);
return response.data;
} catch (error) {
return rejectWithValue(error.response.data);
}
}
);
// Function for resending otp
export const retrievePassword = createAsyncThunk(
"auth/retrievePasswordStatus",
async (data, { rejectWithValue }) => {
try {
const response = await authAPI.retrievePassword(data);
return response.data;
} catch (error) {
return rejectWithValue(error.response.data);
}
}
);
// Function for generating authentication token when reseting forgotten password
export const generateToken = createAsyncThunk(
"auth/generateTokenStatus",
async (data, { rejectWithValue }) => {
try {
const response = await authAPI.generateToken(data);
return response.data;
} catch (error) {
return rejectWithValue(error.response.data);
}
}
);
// Function for creating new password
export const createNewPassword = createAsyncThunk(
"auth/createNewPasswordStatus",
async (token, data, { rejectWithValue }) => {
try {
const response = await authAPI.createNewPassword(token, data);
return response.data;
} catch (error) {
return rejectWithValue(error.response.data);
}
}
);
// Function for updating user profile
export const updateAccount = createAsyncThunk(
"auth/updateAccountStatus",
async (data, { rejectWithValue }) => {
try {
const response = await authAPI.editAccount(data);
return response.data;
} catch (error) {
return rejectWithValue(error.response.data);
}
}
);
// Function for changing the logged in user password
export const changePassword = createAsyncThunk(
"auth/changePasswordStatus",
async (data, { rejectWithValue }) => {
try {
const response = await authAPI.changePassword(data);
return response.data;
} catch (error) {
return rejectWithValue(error.response.data);
}
}
);
// Function for uploading user profile picture
export const photoUpload = createAsyncThunk(
"auth/uploadPhotoStatus",
async (data, { rejectWithValue }) => {
try {
const response = await authAPI.uploadPicture(data);
return response.data;
} catch (error) {
return rejectWithValue(error.response.data);
}
}
)
// The initial state of the reducer
const initialState = {
token: localStorage.getItem("token"),
isAuthenticated: false,
loading: false,
message: {},
errors: [],
user: null,
passwordResetToken: '',
accountUpdating: false,
passwordChangeLoading: false,
photoUploadMessage: {},
photoUploadLoading: false
};
// The auth slice
export const authSlice = createSlice({
name: "auth",
initialState,
reducers: {
clearMessages: (state) => {
state.message = {};
state.errors = [];
state.photoUploadMessage = {};
},
logout: (state) => {
state.token = localStorage.removeItem("token");
state.isAuthenticated = false;
state.user = null;
}
},
extraReducers: (builder) => {
builder.addCase(loginUser.pending, (state, { payload }) => {
state.user = null;
state.message = {};
state.errors = [];
state.loading = true;
});
builder.addCase(loginUser.fulfilled, (state, { payload }) => {
if (payload.status === 'created') {
state.message = payload;
} else {
localStorage.setItem("token", payload.data);
}
state.loading = false;
state.isAuthenticated = true;
});
builder.addCase(loginUser.rejected, (state, { payload }) => {
if (payload.errors.length === 1) {
state.message = payload.errors[0];
} else {
state.errors = payload.errors;
}
state.loading = false;
state.isAuthenticated = false;
});
builder.addCase(loadUser.pending, (state, { payload }) => {
state.errors = [];
state.message = {};
state.loading = true;
state.isAuthenticated = false;
});
builder.addCase(loadUser.fulfilled, (state, { payload }) => {
state.user = payload.data;
state.isAuthenticated = true;
state.loading = false;
});
builder.addCase(loadUser.rejected, (state, { payload }) => {
state.loading = false;
state.message = payload;
state.user = null;
});
builder.addCase(signupUser.pending, (state, { payload }) => {
state.message = {};
state.user = null;
state.errors = [];
state.loading = true;
});
builder.addCase(signupUser.fulfilled, (state, { payload }) => {
state.loading = false;
state.message = payload;
state.user = { phonenumber: payload.data };
});
builder.addCase(signupUser.rejected, (state, { payload }) => {
state.loading = false;
if (payload.errors.length === 1) {
state.message = payload.errors[0];
} else {
state.errors = payload.errors;
}
});
builder.addCase(verifyUser.pending, (state, { payload }) => {
state.message = {};
state.errors = [];
state.loading = true;
});
builder.addCase(verifyUser.fulfilled, (state, { payload }) => {
state.user = null;
state.message = payload;
state.loading = false;
});
builder.addCase(verifyUser.rejected, (state, { payload }) => {
state.message = payload.errors[0];
state.loading = false;
});
builder.addCase(resendOtp.pending, (state, { payload }) => {
state.message = {};
state.loading = true;
state.errors = [];
});
builder.addCase(resendOtp.fulfilled, (state, { payload }) => {
state.loading = false;
state.message = payload;
});
builder.addCase(resendOtp.rejected, (state, { payload }) => {
state.loading = false;
state.message = payload.errors[0];
});
builder.addCase(retrievePassword.pending, (state, { payload }) => {
state.message = {};
state.loading = true;
state.errors = [];
});
builder.addCase(retrievePassword.fulfilled, (state, { payload }) => {
state.loading = false;
state.message = payload;
});
builder.addCase(retrievePassword.rejected, (state, { payload }) => {
state.loading = false;
state.errors = payload.errors;
});
builder.addCase(generateToken.pending, (state, { payload }) => {
state.message = {};
state.passwordResetToken = '';
state.loading = true;
state.errors = [];
});
builder.addCase(generateToken.fulfilled, (state, { payload }) => {
state.loading = false;
state.passwordResetToken = payload.data;
});
builder.addCase(generateToken.rejected, (state, { payload }) => {
state.loading = false;
state.errors = payload.errors;
});
builder.addCase(createNewPassword.pending, (state, { payload }) => {
state.message = {};
state.loading = true;
state.errors = [];
});
builder.addCase(createNewPassword.fulfilled, (state, { payload }) => {
state.loading = false;
state.message = payload;
});
builder.addCase(createNewPassword.rejected, (state, { payload }) => {
state.loading = false;
state.errors.push(payload);
});
builder.addCase(updateAccount.pending, (state, { payload }) => {
state.accountUpdating = true;
});
builder.addCase(updateAccount.fulfilled, (state, { payload }) => {
state.accountUpdating = false;
state.user = payload.data;
const { data, ...rest } = payload;
state.message = rest;
});
builder.addCase(updateAccount.rejected, (state, { payload }) => {
state.accountUpdating = false;
if (payload.errors && payload.errors.length > 0) {
state.errors = payload.errors;
} else {
state.message = payload;
}
});
builder.addCase(changePassword.pending, (state, { payload }) => {
state.passwordChangeLoading = true;
});
builder.addCase(changePassword.fulfilled, (state, { payload }) => {
state.passwordChangeLoading = false;
state.message = payload;
});
builder.addCase(changePassword.rejected, (state, { payload }) => {
state.passwordChangeLoading = false;
if (payload.errors) {
state.errors = payload.errors;
} else {
state.message = payload;
}
});
builder.addCase(photoUpload.pending, (state, { payload }) => {
state.photoUploadLoading = true;
});
builder.addCase(photoUpload.fulfilled, (state, { payload }) => {
state.photoUploadLoading = false;
const { data, ...rest } = payload;
state.photoUploadMessage = rest;
state.user = data;
});
builder.addCase(photoUpload.rejected, (state, { payload }) => {
state.photoUploadLoading = false;
state.photoUploadMessage = payload;
});
}
});
export const { clearMessages, logout } = authSlice.actions;
export default authSlice.reducer;
I tried creating a private route and preventing the loadUser action from running on refresh if the user is already logged in by creating a condition on the App.js; but this method still prove to be ineffective in solving the problem.
Use a loading
state that is initially true so that the useEffect
in App
has a chance to run and check the auth status prior to the PrivateRoute
component checking the isAuthenticated
state and redirecting. You may also want to initialize isAuthenticated
to true of there was a persisted token in localStorage.
authSlice.js
const initialState = {
token: localStorage.getItem("token"),
isAuthenticated: !!localStorage.getItem("token"),
loading: true,
message: {},
errors: [],
user: null,
passwordResetToken: '',
accountUpdating: false,
passwordChangeLoading: false,
photoUploadMessage: {},
photoUploadLoading: false
};
const PrivateRoute = ({ children }) => {
const { loading, isAuthenticated } = useSelector((state) => state.auth);
if (loading) return <Spinner />;
if (!isAuthenticated) return <Navigate to='/login' />;
return children;
}