Published: February 16 2023

React 18 + Redux - Basic HTTP Authentication Example & Tutorial

Tutorial built with React 18.2, Redux 4.2 and Redux Toolkit 1.9.2

This tutorial shows how to build a simple login application with React 18, Redux and the Redux Toolkit that uses Basic HTTP authentication.

Tutorial contents


Example React 18 + Redux App Overview

The example app is pretty minimal and contains just 2 pages to demonstrate Basic HTTP Authentication in React 18 and Redux:

  • /login - public login page with username and password fields, on submit the page sends a POST request to the API to authenticate user credentials, on success the API returns the user details and basic auth data (base64 encoded credentials) to make authenticated requests to secure API routes.
  • / - secure home page that displays a list of users fetched from a secure API endpoint using the basic auth data received after successful login.

Redux State Management with Redux Toolkit

Redux is a state management library for managing global state in a React application. The Redux Toolkit was created to simplify working with Redux and reduce the amount of boilerplate code required.

State and business logic are defined in Redux using a centralized store. With the Redux Toolkit a store is made up of one or more slices, each slice manages the state for a section of the store. State is updated in Redux with actions and reducers, when an action is dispatched the Redux store executes a corresponding reducer function to update the state. Reducers cannot be called directly, they are called by Redux as the result of an action.

For more info on Redux see https://redux.js.org. For more info on slices and the Redux Toolkit see https://redux-toolkit.js.org.

Login Form with React Hook Form Library

The login form in the example is built with React Hook Form - a library for building, validating and handling forms in React using React Hooks. I've been using it for my React projects for the last couple of years, I think it's easier to use than the other options available and requires less code. For more info see https://react-hook-form.com.

Fake Backend API

The React + Redux example app runs with a fake backend by default to enable it to run completely in the browser without a real backend API, to switch to a real backend API you just have to remove or comment out the 2 lines below the comment // setup fake backend located in the main index file (/src/index.js).

You can build your own API or hook it up with a .NET API or Node.js API available below.

Code on GitHub

The example project is available on GitHub at https://github.com/cornflourblue/react-18-redux-basic-authentication-example.

Here it is in action:(See on StackBlitz at https://stackblitz.com/edit/react-18-redux-basic-authentication-example)


Run the React 18 + Redux Basic Auth Example Locally

  1. Install Node.js and npm from https://nodejs.org.
  2. Download or clone the project source code from https://github.com/cornflourblue/react-18-redux-basic-authentication-example
  3. Install all required npm packages by running npm install from the command line in the project root folder (where the package.json is located).
  4. Start the application by running npm start from the command line in the project root folder.
  5. Open a browser and go to the application at http://localhost:3000


Connect the React + Redux App with a Node.js API

For full details about the example Node.js API see the post NodeJS - Basic Authentication Tutorial with Example API. But to get up and running quickly just follow the below steps.

  1. Install NodeJS and NPM from https://nodejs.org.
  2. Download or clone the project source code from https://github.com/cornflourblue/node-basic-authentication-api
  3. Start the api by running npm start from the command line in the project root folder, you should see the message Server listening on port 4000.
  4. Back in the React 18 + Redux example app, remove or comment out the 2 lines below the comment // setup fake backend located in the /src/index.js file, then start the React app and it should now be hooked up with the Node.js API.


Connect the React + Redux App with an ASP.NET Core API

For full details about the example .NET 6 API see the post .NET 6.0 - Basic Authentication Tutorial with Example API. But to get up and running quickly just follow the below steps.

  1. Install the .NET SDK from https://dotnet.microsoft.com/download.
  2. Download or clone the project source code from https://github.com/cornflourblue/dotnet-6-basic-authentication-api
  3. Start the api by running dotnet run from the command line in the project root folder (where the WebApi.csproj file is located), you should see the message Now listening on: http://localhost:4000.
  4. Back in the React 18 + Redux example app, remove or comment out the 2 lines below the comment // setup fake backend located in the /src/index.js file, then start the React app and it should now be hooked up with the .NET API.


React 18 + Redux App Code Documentation

Create React App was used to generate the base project structure with the npx create-react-app <project name> command, the tool is also used to build and serve the application. For more info about Create React App see https://create-react-app.dev/.

The project source (/src) is organised into the following folders:

  • _components
    React components used by pages or by other react components.
  • _helpers
    Anything that doesn't fit into the other folders and doesn't justify having its own folder.
  • _store
    Redux store and slices that define the global state available to the React application. Each slice contains actions and reducers that are responsible for updating global state. For more info on Redux see https://redux.js.org.
  • home
    Components used only by the home page
  • login
    Components used only by the login page

Folder naming convention

Each feature has its own folder (home & login), other shared/common code such as components, helpers, store etc are placed in folders prefixed with an underscore _ to easily differentiate them from features and to group them together at the top of the folder structure.

Javascript code structure

JavaScript files are organised with export statements at the top so it's easy to see all exported modules when you open a file. Export statements are followed by functions and other implementation code for each JS module.

Barrel files

The index.js file in each folder are barrel files that re-export all of the modules from that folder so they can be imported using only the folder path instead of the full path to each module, and to enable importing multiple modules in a single import (e.g. import { Nav, PrivateRoute } from '_components';). The /_store/index.js file also configures and exports the centralized Redux store which is provided to the React app in the main index.js file on startup.

Base URL for imports

The baseUrl is set to "src" in the jsconfig.json file to make all import statements (without a dot '.' prefix) relative to the /src folder of the project, removing the need for long relative paths like import { history } from '../../../_helpers';.

Click any of the below links to jump down to a description of each file along with its code:

 

Main Index Html File

Path: /public/index.html

The main index.html file is the initial page loaded by the browser that kicks everything off. Create React App (with Webpack under the hood) bundles all of the compiled javascript files together and injects them into the body of the index.html page so the scripts can be loaded and executed by the browser.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>React 18 + Redux - Basic Authentication Example</title>

    <!-- bootstrap css -->
    <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>

    <!-- credits -->
    <div class="text-center mt-4">
        <p>
            <a href="https://jasonwatmore.com/react-18-redux-basic-http-authentication-example-tutorial" target="_blank">React 18 + Redux - Basic HTTP Authentication Example & Tutorial</a>
        </p>
        <p>
            <a href="https://jasonwatmore.com" target="_blank">JasonWatmore.com</a>
        </p>
    </div>
</body>
</html>
 

Nav Component

Path: /src/_components/Nav.jsx

The nav component displays the primary bar in the example. The component gets the current authUser from global Redux state with the useSelector() hook and only displays the nav if the user is logged in.

The react router NavLink component automatically adds the active class to the active nav item so it is highlighted in the UI.

import { NavLink } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux';

import { authActions } from '_store';

export { Nav };

function Nav() {
    const authUser = useSelector(x => x.auth.user);
    const dispatch = useDispatch();
    const logout = () => dispatch(authActions.logout());

    // only show nav when logged in
    if (!authUser) return null;
    
    return (
        <nav className="navbar navbar-expand navbar-dark bg-dark px-3">
            <div className="navbar-nav">
                <NavLink to="/" className="nav-item nav-link">Home</NavLink>
                <button onClick={logout} className="btn btn-link nav-item nav-link">Logout</button>
            </div>
        </nav>
    );
}
 

Private Route

Path: /src/_components/PrivateRoute.jsx

The react private route component renders child components (children) if the user is logged in. If not logged in the user is redirected to the /login page with the return url passed in the location state property.

The current logged in user (authUser) is retrieved from Redux with a call to the useSelector() hook.

import { Navigate } from 'react-router-dom';
import { useSelector } from 'react-redux';

import { history } from '_helpers';

export { PrivateRoute };

function PrivateRoute({ children }) {
    const { user: authUser } = useSelector(x => x.auth);
    
    if (!authUser) {
        // not logged in so redirect to login page with the return url
        return <Navigate to="/login" state={{ from: history.location }} />
    }

    // authorized so return child components
    return children;
}
 

Fake Backend

Path: /src/_helpers/fake-backend.js

In order to run and test the React + Redux app without a real backend API, the example uses a fake backend that intercepts the HTTP requests from the React app and sends back "fake" responses. This is done by monkey patching the window.fetch() function to return fake responses for a specific set of routes.

Monkey Patching

Monkey patching is a technique used to alter the behaviour of an existing function either to extend it or change the way it works. In JavaScript this is done by storing a reference to the original function in a variable and replacing the original function with a new custom function that (optionally) calls the original function before/after executing some custom code.

The fake backend is organised into a top level handleRoute() function that checks the request url and method to determine how the request should be handled. For fake routes one of the below // route functions is called, for all other routes the request is passed through to the real backend by calling the original fetch request function (realFetch(url, opts)). Below the route functions there are // helper functions for returning different response types and performing small tasks.

export { fakeBackend };

function fakeBackend() {
    let users = [{ id: 1, username: 'test', password: 'test', firstName: 'Test', lastName: 'User' }];
    let realFetch = window.fetch;
    window.fetch = function (url, opts) {
        return new Promise((resolve, reject) => {
            // wrap in timeout to simulate server api call
            setTimeout(handleRoute, 500);

            function handleRoute() {
                switch (true) {
                    case url.endsWith('/users/authenticate') && opts.method === 'POST':
                        return authenticate();
                    case url.endsWith('/users') && opts.method === 'GET':
                        return getUsers();
                    default:
                        // pass through any requests not handled above
                        return realFetch(url, opts)
                            .then(response => resolve(response))
                            .catch(error => reject(error));
                }
            }

            // route functions

            function authenticate() {
                const { username, password } = body();
                const user = users.find(x => x.username === username && x.password === password);

                if (!user) return error('Username or password is incorrect');

                return ok({
                    id: user.id,
                    username: user.username,
                    firstName: user.firstName,
                    lastName: user.lastName,
                    authdata: btoa(`${user.username}:${user.password}`)
                });
            }

            function getUsers() {
                if (!isAuthenticated()) return unauthorized();
                return ok(users);
            }

            // helper functions

            function ok(body) {
                resolve({ ok: true, json: () => Promise.resolve(body), ...headers() })
            }

            function unauthorized() {
                resolve({ status: 401, json: () => Promise.resolve({ message: 'Unauthorized' }), ...headers() })
            }

            function error(message) {
                resolve({ status: 400, json: () => Promise.resolve({ message }), ...headers() })
            }

            function isAuthenticated() {
                return opts.headers['Authorization'] === `Basic ${btoa('test:test')}`;
            }

            function body() {
                return opts.body && JSON.parse(opts.body);    
            }

            function headers() {
                return { headers: { get: () => 'application/json' } };
            }            
        });
    }
}
 

Fetch Wrapper

Path: /src/_helpers/fetch-wrapper.js

The fetch wrapper is a lightweight wrapper around the native browser fetch() function used to simplify the code for making HTTP requests. It returns an object with methods for get, post, put and delete requests, it automatically handles the parsing of JSON data from responses, and throws an error if the HTTP response is not successful (!response.ok). If the response is 401 Unauthorized or 403 Forbidden the user is automatically logged out of the React + Redux app.

With the fetch wrapper a POST request can be made as simply as fetchWrapper.post(url, body);. It's used in the example app in the Redux auth slice and users slice.

The authHeader() function is used to automatically add basic auth credentials to the HTTP Authorization header of the request if the user is logged in and the request is to the application API url.

The basicAuthData() function returns the basic auth data for the current logged in user (the base64 encoded username and password), or null if not logged in. The authdata is retreived from Redux using store.getState() instead of the useSelector() hook because hook functions can only be called from React components or other hook functions.

import { store, authActions } from '_store';

export const fetchWrapper = {
    get: request('GET'),
    post: request('POST'),
    put: request('PUT'),
    delete: request('DELETE')
};

function request(method) {
    return (url, body) => {
        const requestOptions = {
            method,
            headers: authHeader(url)
        };
        if (body) {
            requestOptions.headers['Content-Type'] = 'application/json';
            requestOptions.body = JSON.stringify(body);
        }
        return fetch(url, requestOptions).then(handleResponse);
    }
}

// helper functions

function authHeader(url) {
    // return auth header with basic auth credentials if user is logged in and request is to the api url
    const authData = basicAuthData();
    const isLoggedIn = !!authData;
    const isApiUrl = url.startsWith(process.env.REACT_APP_API_URL);
    if (isLoggedIn && isApiUrl) {
        return { Authorization: `Basic ${authData}` };
    } else {
        return {};
    }
}

function basicAuthData() {
    return store.getState().auth.user?.authdata;
}

async function handleResponse(response) {
    const isJson = response.headers.get('content-type')?.includes('application/json');
    const data = isJson ? await response.json() : null;

    // check for error response
    if (!response.ok) {
        if (basicAuthData() && [401, 403].includes(response.status)) {
            // auto logout if logged in and response status is 401 Unauthorized or 403 Forbidden
            const logout = () => store.dispatch(authActions.logout());
            logout();
        }

        // get error message from body or default to response status
        const error = (data && data.message) || response.status;
        return Promise.reject(error);
    }

    return data;
}
 

History

Path: /src/_helpers/history.js

The history helper is a plain javascript object that provides access to the React Router navigate() function and location property from anywhere in the React app including outside components. It's required in this example to enable navigation on login and logout from the Redux auth slice. The properties are initialized on app startup in the root App component with the React Router useNavigate() and useLocation() hooks.

By default you can only navigate from inside React components or hook functions with the useNavigate() hook. There is a HistoryRouter available in React Router 6 that accepts a custom history object to allow navigation outside components. However (at the time of writing) the HistoryRouter component is prefixed with unstable_ because it still has some issues so I decided to go with an alternative solution.

// custom history object to allow navigation outside react components
export const history = {
    navigate: null,
    location: null
};
 

Redux Auth Slice

Path: /src/_store/auth.slice.js

The auth slice manages Redux state, actions and reducers for authentication. The file is organised into three sections to make it easier to see what's going on. The first section calls functions to create and configure the slice, the second section exports the actions and reducer, and the third section contains the functions that implement the logic.

initialState defines the state properties in the slice with their initial values. The user state property holds the current logged in user, it is initialized with the 'user' object from local storage to support staying logged in between page refreshes and browser sessions, or null if localStorage is empty. The error is displayed in the login component if login failed.

The reducers object passed to createSlice() contains logic for synchronous actions (things you don't have to wait for). For example the logout reducer sets the user state property to null, removes it from local storage and redirects to the login page. It doesn't perform any async tasks such as an API request. The createSlice() function auto generates matching actions for these reducers and exposes them via the slice.actions property.

Async Actions with createAsyncThunk()

The extraActions object contains logic for asynchronous actions (things you have to wait for) such as API requests. Async actions are created with the Redux Toolkit createAsyncThunk() function. The extraReducers object contains methods for updating Redux state at different stages of async actions (pending, fulfilled, rejected), and is passed as a parameter to the createSlice() function.

The login() action method posts credentials to the API, on success (fulfilled) the returned user object is stored in the Redux state user prop and localStorage, and the user is redirected to the return url or home page. On fail (rejected) the error is stored in the Redux state error property which is displayed inside the login component.

Export Actions and Reducer for Redux Slice

The authActions export includes all sync actions (slice.actions) and async actions (extraActions) for the auth slice.

The reducer for the auth slice is exported as authReducer, which is used in the Redux store for the app to configure the global state store.

import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';

import { history, fetchWrapper } from '_helpers';

// create slice

const name = 'auth';
const initialState = createInitialState();
const reducers = createReducers();
const extraActions = createExtraActions();
const extraReducers = createExtraReducers();
const slice = createSlice({ name, initialState, reducers, extraReducers });

// exports

export const authActions = { ...slice.actions, ...extraActions };
export const authReducer = slice.reducer;

// implementation

function createInitialState() {
    return {
        // initialize state from local storage to enable user to stay logged in
        user: JSON.parse(localStorage.getItem('user')),
        error: null
    }
}

function createReducers() {
    return {
        logout
    };

    function logout(state) {
        state.user = null;
        localStorage.removeItem('user');
        history.navigate('/login');
    }
}

function createExtraActions() {
    const baseUrl = `${process.env.REACT_APP_API_URL}/users`;

    return {
        login: login()
    };

    function login() {
        return createAsyncThunk(
            `${name}/login`,
            async ({ username, password }) => await fetchWrapper.post(`${baseUrl}/authenticate`, { username, password })
        );
    }
}

function createExtraReducers() {
    return (builder) => {
        login();

        function login() {
            var { pending, fulfilled, rejected } = extraActions.login;
            builder
                .addCase(pending, (state) => {
                    state.error = null;
                })
                .addCase(fulfilled, (state, action) => {
                    const user = action.payload;

                    // store user details and basic auth data in local storage to keep user logged in between page refreshes
                    localStorage.setItem('user', JSON.stringify(user));
                    state.user = user;

                    // get return url from location state or default to home page
                    const { from } = history.location.state || { from: { pathname: '/' } };
                    history.navigate(from);
                })
                .addCase(rejected, (state, action) => {
                    state.error = action.error;
                });
        }
    };
}
 

Redux Users Slice

Path: /src/_store/users.slice.js

The users slice manages Redux state, actions and reducers for users in the React app. Each part of the slice is organised into its own function that is called from the top of the file to make it easier to see what's going on. initialState defines the state properties in this slice with their initial values. The users property is used to store all users fetched from the API. It defaults to an empty object and can hold one the following values:

  • {} - initial state.
  • { loading: true } - users are currently being fetched from the API.
  • [{ ... }, { ... }, { ... }] - array of users returned by the API.
  • { error: { message: 'an error message' } } - request to the API failed and an error was returned.

Async Actions with createAsyncThunk()

The extraActions object contains logic for asynchronous actions (things you have to wait for) such as API requests. Async actions are created with the Redux Toolkit createAsyncThunk() function. The extraReducers object contains methods for updating Redux state at different stages of async actions (pending, fulfilled, rejected), and is passed as a parameter to the createSlice() function.

The getAll() action method fetches the users from the API, on success (fulfilled) the returned users (action.payload) array is stored in the Redux state users property.

Export Actions and Reducer for Redux Slice

The userActions export includes all sync actions (slice.actions) and async actions (extraActions) for the users slice.

The reducer for the users slice is exported as usersReducer, which is used in the root Redux store to configure global state for the React app.

import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';

import { fetchWrapper } from '_helpers';

// create slice

const name = 'users';
const initialState = createInitialState();
const extraActions = createExtraActions();
const extraReducers = createExtraReducers();
const slice = createSlice({ name, initialState, extraReducers });

// exports

export const userActions = { ...slice.actions, ...extraActions };
export const usersReducer = slice.reducer;

// implementation

function createInitialState() {
    return {
        users: {}
    }
}

function createExtraActions() {
    const baseUrl = `${process.env.REACT_APP_API_URL}/users`;

    return {
        getAll: getAll()
    };

    function getAll() {
        return createAsyncThunk(
            `${name}/getAll`,
            async () => await fetchWrapper.get(baseUrl)
        );
    }
}

function createExtraReducers() {
    return (builder) => {
        getAll();

        function getAll() {
            var { pending, fulfilled, rejected } = extraActions.getAll;
            builder
                .addCase(pending, (state) => {
                    state.users = { loading: true };
                })
                .addCase(fulfilled, (state, action) => {
                    state.users = action.payload;
                })
                .addCase(rejected, (state, action) => {
                    state.users = { error: action.error };
                });
        };
    }
}
 

Redux Store

Path: /src/_store/index.js

The store index file configures the root Redux store for the React application with the configureStore() function. The returned Redux store contains the state properties auth and users which map to their corresponding slices.

The index file also re-exports all of the modules from the Redux slices in the folder. This enables Redux modules to be imported directly from the _store folder without the path to the slice file. It also enables multiple imports from different files at once (e.g. import { store, authActions } from '_store';)

import { configureStore } from '@reduxjs/toolkit';

import { authReducer } from './auth.slice';
import { usersReducer } from './users.slice';

export * from './auth.slice';
export * from './users.slice';

export const store = configureStore({
    reducer: {
        auth: authReducer,
        users: usersReducer
    },
});
 

Home Component

Path: /src/home/Home.jsx

The home page is displayed after signing in to the application, it shows the signed in user's name plus a list of all users in the tutorial application. The users are loaded into Redux state by calling dispatch(userActions.getAll()); from the useEffect() hook function, see how the users are fetched and the Redux state is updated in users slice.

The users list is displayed if the users state property contains an array with at least 1 item, which is confirmed by checking {users.length && ...}.

A loading spinner is displayed while the API request for users is in progress/loading, and an error message is displayed if the request fails.

Redux state values are retrieved for auth and users data with the help of the useSelector() hook function.

import { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';

import { userActions } from '_store';

export { Home };

function Home() {
    const dispatch = useDispatch();
    const { user: authUser } = useSelector(x => x.auth);
    const { users } = useSelector(x => x.users);

    useEffect(() => {
        dispatch(userActions.getAll());
        
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, []);

    return (
        <div>
            <h1>Hi {authUser?.firstName}!</h1>
            <p>You're logged in with React 18 + Redux & Basic Auth!!</p>
            <h3>Users from secure api end point:</h3>
            {users.length &&
                <ul>
                    {users.map(user =>
                        <li key={user.id}>{user.firstName} {user.lastName}</li>
                    )}
                </ul>
            }
            {users.loading && <div className="spinner-border spinner-border-sm"></div>}
            {users.error && <div className="text-danger">Error loading users: {users.error.message}</div>}
        </div>
    );
}
 

Login Component

Path: /src/login/Login.jsx

The login page contains a form built with the React Hook Form library that contains username and password fields for logging into the React + Redux app.

Form validation rules are defined with the Yup schema validation library and passed with formOptions to the React Hook Form useForm() function, for more info on Yup see https://github.com/jquense/yup.

The useForm() hook function returns an object with methods for working with a form including registering inputs, handling form submit, accessing form state, displaying errors and more, for a complete list see https://react-hook-form.com/api/useform.

The onSubmit function gets called when the form is submitted and valid, and submits user credentials to the api by calling dispatch(authActions.login({ username, password })). On successful authentication the user details with basic auth data are stored in Redux shared state by the login.fulfilled reducer in the auth slice.

The returned JSX template contains the markup for page including the form, input fields and validation messages. The form fields are registered with the React Hook Form by calling the register function with the field name from each input element (e.g. {...register('username')}). For more info on form validation with React Hook Form see React Hook Form 7 - Form Validation Example.

import { useEffect } from 'react';
import { useForm } from "react-hook-form";
import { yupResolver } from '@hookform/resolvers/yup';
import * as Yup from 'yup';
import { useSelector, useDispatch } from 'react-redux';

import { history } from '_helpers';
import { authActions } from '_store';

export { Login };

function Login() {
    const dispatch = useDispatch();
    const authUser = useSelector(x => x.auth.user);
    const authError = useSelector(x => x.auth.error);

    useEffect(() => {
        // redirect to home if already logged in
        if (authUser) history.navigate('/');

        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, []);

    // form validation rules 
    const validationSchema = Yup.object().shape({
        username: Yup.string().required('Username is required'),
        password: Yup.string().required('Password is required')
    });
    const formOptions = { resolver: yupResolver(validationSchema) };

    // get functions to build form with useForm() hook
    const { register, handleSubmit, formState } = useForm(formOptions);
    const { errors, isSubmitting } = formState;

    function onSubmit({ username, password }) {
        return dispatch(authActions.login({ username, password }));
    }

    return (
        <div className="col-md-6 offset-md-3 mt-5">
            <div className="alert alert-info">
                Username: test<br />
                Password: test
            </div>
            <div className="card">
                <h4 className="card-header">Login</h4>
                <div className="card-body">
                    <form onSubmit={handleSubmit(onSubmit)}>
                        <div className="mb-3">
                            <label className="form-label">Username</label>
                            <input name="username" type="text" {...register('username')} className={`form-control ${errors.username ? 'is-invalid' : ''}`} />
                            <div className="invalid-feedback">{errors.username?.message}</div>
                        </div>
                        <div className="mb-3">
                            <label className="form-label">Password</label>
                            <input name="password" type="password" {...register('password')} className={`form-control ${errors.password ? 'is-invalid' : ''}`} />
                            <div className="invalid-feedback">{errors.password?.message}</div>
                        </div>
                        <button disabled={isSubmitting} className="btn btn-primary">
                            {isSubmitting && <span className="spinner-border spinner-border-sm me-1"></span>}
                            Login
                        </button>
                        {authError &&
                            <div className="alert alert-danger mt-3 mb-0">{authError.message}</div>
                        }
                    </form>
                </div>
            </div>
        </div>
    )
}
 

App Component

Path: /src/App.jsx

The App component is the root component of the example app, it contains the outer html, main nav and routes for the application.

The /login route is public, and the home route (/) is secured by the private route component that uses Redux to check if the user is logged in.

The last route is a catch-all redirect route that redirects any unmatched paths to the home page.

import { Routes, Route, Navigate, useNavigate, useLocation } from 'react-router-dom';

import { history } from '_helpers';
import { Nav, PrivateRoute } from '_components';
import { Home } from 'home';
import { Login } from 'login';

export { App };

function App() {
    // init custom history object to allow navigation from 
    // anywhere in the react app (inside or outside components)
    history.navigate = useNavigate();
    history.location = useLocation();

    return (
        <div className="app-container bg-light">
            <Nav />
            <div className="container pt-4 pb-4">
                <Routes>
                    <Route
                        path="/"
                        element={
                            <PrivateRoute>
                                <Home />
                            </PrivateRoute>
                        }
                    />
                    <Route path="/login" element={<Login />} />
                    <Route path="*" element={<Navigate to="/" />} />
                </Routes>
            </div>
        </div>
    );
}
 

Global CSS Styles

Path: /src/index.css

The global stylesheet file contains CSS styles that are applied globally throughout the React application, it is imported in the main index.js file below.

.app-container {
    min-height: 350px;
}
 

Main index.js file

Path: /src/index.js

The main index.js file bootstraps the React + Redux app by rendering the App component in the root div element located in the main index html file.

The Provider component is the context provider for Redux state and is a required ancestor for any React components that access Redux state. Wrapping it around the root App component makes the Redux store global so it's accessible to all components in the React app.

The React.StrictMode component doesn't render any elements in the UI, it runs in development mode to highlight potential issues/bugs in the React app. For more info see https://reactjs.org/docs/strict-mode.html.

BrowserRouter adds support for Routes and React Router 6 features from any component in the app.

Before the React app is started, the global CSS stylesheet (./index.css) is imported and the fake backend API is enabled. To disable the fake backend simply remove or comment out the 2 lines below the comment // setup fake backend.

import React from 'react';
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
import { BrowserRouter } from 'react-router-dom';

import { store } from './_store';
import { App } from './App';
import './index.css';

// setup fake backend
import { fakeBackend } from './_helpers';
fakeBackend();

const container = document.getElementById('root');
const root = createRoot(container);

root.render(
    <React.StrictMode>
        <Provider store={store}>
            <BrowserRouter>
                <App />
            </BrowserRouter>
        </Provider>
    </React.StrictMode>
);
 

dotenv

Path: /.env

The dotenv file contains environment variables used in the example React app, the API URL is used in the Redux auth slice and users slice to send HTTP requests to the API.

Environment variables set in the dotenv file that are prefixed with REACT_APP_ are accessible in the React app via process.env.<variable name> (e.g. process.env.REACT_APP_API_URL). For more info on using environment variables in React see https://create-react-app.dev/docs/adding-custom-environment-variables/

REACT_APP_API_URL=http://localhost:4000
 

jsconfig.json

Path: /jsconfig.json

The below configuration enables support for absolute imports to the application, so modules can be imported with absolute paths instead of relative paths (e.g. import { MyComponent } from '_components'; instead of import { MyComponent } from '../../../_components';).

For more info on absolute imports in React see https://create-react-app.dev/docs/importing-a-component/#absolute-imports.

{
    "compilerOptions": {
        "baseUrl": "src"
    },
    "include": ["src"]
}
 

Package.json

Path: /package.json

The package.json file contains project configuration information including package dependencies that get installed when you run npm install and scripts that are executed when you run npm start or npm run build etc. Full documentation is available at https://docs.npmjs.com/files/package.json.

{
    "name": "react-18-redux-basic-authentication-example",
    "version": "0.1.0",
    "private": true,
    "dependencies": {
        "@hookform/resolvers": "^2.9.11",
        "@reduxjs/toolkit": "^1.9.2",
        "react": "^18.2.0",
        "react-dom": "^18.2.0",
        "react-hook-form": "^7.43.1",
        "react-redux": "^8.0.5",
        "react-router-dom": "^6.8.1",
        "react-scripts": "5.0.1",
        "yup": "^0.32.11"
    },
    "scripts": {
        "start": "react-scripts start",
        "build": "react-scripts build",
        "eject": "react-scripts eject"
    },
    "eslintConfig": {
        "extends": [
            "react-app"
        ]
    },
    "browserslist": {
        "production": [
            ">0.2%",
            "not dead",
            "not op_mini all"
        ],
        "development": [
            "last 1 chrome version",
            "last 1 firefox version",
            "last 1 safari version"
        ]
    }
}

 


Need Some React Help?

Search fiverr for freelance React developers.


Follow me for updates

On Twitter or RSS.


When I'm not coding...

Me and Tina are on a motorcycle adventure around Australia.
Come along for the ride!


Comments


Supported by