Managing Authentication Context in React with Redux: A Step-by-Step Guide
Sanjay Jaiswar
REACT | ANGULAR | JAVASCRIPT | REDUX | NODE | EXPRESS | LARAVEL | PHP | SHOPIFY | MAGENTO | WORDPRESS | PYTHON | MYSQL | MONGODB | REDIS | RESTAPI | JWT | GIT | AWS | EC2 | RDS | BEANSTALK | CICD | INTEGRATIONS
Authentication is a fundamental requirement for any modern web application, ensuring that users have secure access to the right features and data. While React provides a straightforward way to manage state through Context, using Redux for authentication can offer enhanced scalability and control, especially in larger applications. In this guide, we'll explore how to implement authentication in React using Redux, breaking down the process into clear, manageable steps. Whether you're new to Redux or looking to refine your approach, this article will equip you with the knowledge to build a robust authentication system that keeps your application secure and user-friendly.
I have used SB-ADMIN dashboard for this example which displays orders and products after Login
1) To work with React-Redux we need to install following library using npm
npm install redux react-redux @reduxjs/toolkit
2) In the app/src directory, create a new folder called slices, where we will define our state and actions for the reducer.
//src/slices/authSlice.js
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
currentUser: null,
token: null,
expirationTime: null,
};
const isTokenExpired = (expirationTime) => {
return expirationTime && expirationTime <= Date.now();
};
const loadStateFromLocalStorage = () => {
try {
const serializedState = localStorage.getItem('authState');
if (serializedState) {
return JSON.parse(serializedState);
}
} catch (e) {
console.error('Failed to load state from localStorage', e);
}
return initialState;
};
const authSlice = createSlice({
name: 'auth',
initialState: loadStateFromLocalStorage(),
reducers: {
login(state, action) {
state.currentUser = action.payload.user;
state.token = action.payload.token;
state.expirationTime = action.payload.expirationTime;
localStorage.setItem('authState', JSON.stringify(state));
},
logout(state) {
state.currentUser = null;
state.token = null;
state.expirationTime = null;
localStorage.removeItem('authState');
},
setTokenExpiration(state) {
if (isTokenExpired(state.expirationTime)) {
state.currentUser = null;
state.token = null;
state.expirationTime = null;
localStorage.removeItem('authState');
}
}
}
});
export const { login, logout, setTokenExpiration } = authSlice.actions;
export default authSlice.reducer;
3) Next, we'll define a store in src/store.js, which will be provided to the entire app.
//src/store.js
import { configureStore } from '@reduxjs/toolkit';
import authReducer from './slices/authSlice';
const store = configureStore({
reducer: {
auth: authReducer,
},
});
export default store;
4) Provide this store using Provider in index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals'; tool
import { Provider } from 'react-redux';
import store from './store';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<App />
</Provider>
);
reportWebVitals();
5) App.js
import React from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { useSelector } from 'react-redux';
import Login from './components/Auth/Login';
import Dashboard from './components/Admin/Dashboard';
import { AuthProvider } from './context/AuthContext';
function App() {
return (
<AuthProvider>
<Router>
<Routes>
<Route path="/" element={<AuthRedirect />} />
<Route path="/dashboard/*" element={<PrivateRoute component={Dashboard} />} />
<Route path="*" element={<Navigate to="/" />} />
</Routes>
</Router>
</AuthProvider>
);
}
// Component to handle redirect based on authentication status
const AuthRedirect = () => {
const { currentUser } = useSelector(state => state.auth);
return currentUser ? <Navigate to="/dashboard" /> : <Login />;
};
// Component to handle protected routes
const PrivateRoute = ({ component: Component, ...rest }) => {
const { currentUser } = useSelector(state => state.auth);
return currentUser ? <Component {...rest} /> : <Navigate to="/" />;
};
export default App;
6) Login.jsx making use of reducer and dispatch from react-redux, I have created ApiService.js for all API related to Auth. In backed I have used Node.js and Mongo
import React, { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { login } from '../../slices/authSlice';
import ApiService from '../../services/ApiService';
const Login = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [message, setMessage] = useState('');
const navigate = useNavigate();
const dispatch = useDispatch();
const handleLogin = async (e) => {
e.preventDefault();
try {
// Call the login method from ApiService
const response = await ApiService.login({ email, password });
if (response.status === 200) {
const data = response.data;
dispatch(login({
user: data.data, // Adjusted to use `data.data` for user details
token: data.access_token,
expirationTime: Date.now() + data.expires_in * 1000
}));
navigate('/dashboard');
} else {
setMessage('Login failed');
}
} catch (error) {
setMessage(error.message || 'An error occurred');
}
};
return (
<div className="sqtech-login">
<div className="bg-gradient-primary" style={{ height: '100vh' }}>
<div className="container">
<div className="row justify-content-center">
<div className="col-xl-7 col-lg-7 col-md-7">
<div className="card o-hidden border-0 shadow-lg my-5">
<div className="card-body p-0">
<div className="row">
<div className="col-lg-10 offset-lg-1">
<div className="p-5">
<div className="text-center">
<h1 className="h4 text-gray-900 mb-4">Welcome Back!</h1>
</div>
<form className="user" onSubmit={handleLogin}>
<div className="form-group">
<input type="email" className="form-control form-control-user"
id="exampleInputEmail" aria-describedby="emailHelp"
placeholder="Enter Email Address..." value={email} onChange={(e) => setEmail(e.target.value)} required />
</div>
<div className="form-group">
<input type="password" className="form-control form-control-user"
id="exampleInputPassword" placeholder="Password" value={password} onChange={(e) => setPassword(e.target.value)} required />
</div>
<div className="form-group">
<div className="custom-control custom-checkbox small">
<input type="checkbox" className="custom-control-input"
id="customCheck" />
<label className="custom-control-label"
htmlFor="customCheck">Remember Me</label>
</div>
</div>
<button type="submit" className="btn btn-primary btn-user btn-block">Login</button>
<hr />
<button className="btn btn-google btn-user btn-block">
<i className="fab fa-google fa-fw"></i> Login with Google
</button>
<button className="btn btn-facebook btn-user btn-block">
<i className="fab fa-facebook-f fa-fw"></i> Login with Facebook
</button>
</form>
{message && (
<div className="alert alert-danger" role="alert">
{message}
</div>
)}
<hr />
<div className="text-center">
<Link className="small" to="/forgot-password">Forgot Password?</Link>
</div>
<div className="text-center">
<Link className="small" to="/register">Create an Account!</Link>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
export default Login;
7) Handling Logout using dispatch and logout action in reducer
领英推荐
import React, { useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { logout } from '../../../slices/authSlice';
import { useNavigate } from 'react-router-dom';
const Bottom = () => {
const dispatch = useDispatch();
const navigate = useNavigate();
const handleLogout = (event) => {
event.preventDefault();
dispatch(logout());
// Hide the modal
const modal = document.getElementById('logoutModal');
if (modal) {
const bootstrapModal = new window.bootstrap.Modal(modal);
bootstrapModal.hide();
}
// Redirect to home after hiding the modal
navigate('/');
};
useEffect(() => {
// Cleanup code to ensure no leftover backdrop
return () => {
const backdrop = document.querySelector('.modal-backdrop');
if (backdrop) {
document.body.removeChild(backdrop);
}
};
}, []);
return (
<>
<a className="scroll-to-top rounded" href="#page-top">
<i className="fas fa-angle-up"></i>
</a>
<div className="modal fade" id="logoutModal" tabIndex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div className="modal-dialog" role="document">
<div className="modal-content">
<div className="modal-header">
<h5 className="modal-title" id="exampleModalLabel">Ready to Leave?</h5>
<button className="close" type="button" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div className="modal-body">Select "Logout" below if you are ready to end your current session.</div>
<div className="modal-footer">
<button className="btn btn-secondary" type="button" data-dismiss="modal">Cancel</button>
<button className="btn btn-primary" onClick={handleLogout}>Logout</button>
</div>
</div>
</div>
</div>
</>
);
};
export default Bottom;
8) We can use Redux to access details like token and user info in components, services, or providers as long as local storage has these information can be done using following
import { useSelector} from 'react-redux';
// Access User info from Redux
const currentUser = useSelector(state => state.auth);
// Access token from Redux
const { token } = useSelector((state) => state.auth);
9) Run Node.js application on port 5000
npm run dev
Run react application running on port 3000
npm start
10) output
const [ordersResponse] = await Promise.all([
ApiService.getOrders(token, page, search, filters, orderBy)
]);
//getOrders
getOrders: (token, page = 1, search = '', filters = [], orderBy = []) => {
const filterParams = filters.map(f => `filter[]=${f}`).join('&');
const orderByParams = orderBy.map(o => `orderBy[]=${o}`).join('&');
return axios.get(`${API_URL}/dashboard/orders?page=${page}&search=${search}&${filterParams}&${orderByParams}`, {
headers: {
Authorization: `Bearer ${token}`
}
});
},