Managing Authentication Context in React with Redux: A Step-by-Step Guide

Managing Authentication Context in React with Redux: A Step-by-Step Guide

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


Login Screen


Response with token and User information


User Name from Redux using State


Orders information using access token
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}`
            }
        });
    },        


要查看或添加评论,请登录

Sanjay Jaiswar的更多文章

社区洞察

其他会员也浏览了