Publicada:

React 18 + Redux - Ejemplo y tutorial de autenticación JWT

Tutorial creado con React 18.1.0, Redux 4.2.0 y Redux Toolkit 1.8.2

Otras versiones disponibles:

Este tutorial muestra cómo crear una aplicación de inicio de sesión simple con React 18, Redux y Redux Toolkit que usa la autenticación JWT.

Ejemplo React 18 + Aplicación Redux

La aplicación de ejemplo es bastante mínima y contiene solo 2 páginas para demostrar la autenticación JWT en React 18 y Redux:

  • /login - página de inicio de sesión pública con campos de nombre de usuario y contraseña; al enviar, la página envía una solicitud POST a la API para autenticar las credenciales del usuario; si tiene éxito, la API devuelve un token JWT para realizar solicitudes autenticadas. para proteger las rutas API.
  • / - página de inicio segura que muestra una lista de usuarios obtenidos de un punto final API seguro utilizando el token JWT recibido después de un inicio de sesión exitoso.

Gestión de estado de Redux con Redux Toolkit

Redux es una biblioteca de administración de estado para administrar el estado global en una aplicación React. Redux Toolkit se creó para simplificar el trabajo con Redux y reducir la cantidad de código repetitivo necesario.

El estado y la lógica empresarial se definen en Redux mediante un almacén centralizado. Con Redux Toolkit, un almacén se compone de uno o más segmentos, cada segmento administra el estado de una sección del almacén. El estado se actualiza en Redux con acciones y reductores, cuando se envía una acción, el almacén de Redux ejecuta una función de reducción correspondiente para actualizar el estado. Los reductores no se pueden llamar directamente, Redux los llama como resultado de una acción.

Para obtener más información sobre Redux, consulte https://redux.js.org. Para obtener más información sobre los segmentos y el kit de herramientas de Redux, consulte https://redux-toolkit.js.org.

Formulario de inicio de sesión con la biblioteca de formularios React Hook

El formulario de inicio de sesión en el ejemplo está construido con React Hook Form - una biblioteca para construir, validar y manejar formularios en React usando React Hooks. Lo he estado usando para mis proyectos de React durante los últimos años, creo que es más fácil de usar que las otras opciones disponibles y requiere menos código. Para obtener más información, consulte https://react-hook-form.com.

API de servidor falso

La aplicación de ejemplo de React + Redux se ejecuta con un backend falso de forma predeterminada para permitir que se ejecute completamente en el navegador sin una API de backend real (backend-less), para cambiar a una API de backend real, solo tiene que eliminar o comentar las 2 líneas debajo del comentario // setup fake backend ubicado en el archivo de índice principal (/src/index.js). Puede crear su propia API o conectarla con la API de .NET o Node.js disponible (instrucciones a continuación).

Código en GitHub

El proyecto de ejemplo está disponible en GitHub en https://github.com/cornflourblue/react-18-redux-jwt-authentication-example.

Aquí está en acción:(Ver en StackBlitz en https://stackblitz.com/edit/react-18-redux-jwt-authentication-example)


Ejecute el ejemplo de React 18 + Redux JWT localmente

  1. Instalar Node.js y npm desde https://nodejs.org.
  2. Descargue o clone el código fuente del proyecto desde https://github.com/cornflourblue/react-18-redux-jwt-authentication-example
  3. Instale todos los paquetes npm necesarios ejecutando npm install desde la línea de comando en la carpeta raíz del proyecto (donde se encuentra el package.json).
  4. Inicie la aplicación ejecutando npm start desde la línea de comando en la carpeta raíz del proyecto.
  5. Abra un navegador y vaya a la aplicación en http://localhost:3000


Ejecute la aplicación React + Redux con una API de .NET

Para obtener detalles completos sobre el ejemplo de .NET JWT Auth API, consulte la publicación .NET 6.0 - Tutorial de autenticación JWT con API de ejemplo. Pero para ponerse en marcha rápidamente, solo siga los pasos a continuación.

  1. Instale el SDK de .NET desde https://dotnet.microsoft.com/download.
  2. Descargue o clone el código fuente del proyecto desde https://github.com/cornflourblue/dotnet-6-jwt-authentication-api
  3. Inicie la API ejecutando dotnet run desde la línea de comando en la carpeta raíz del proyecto (donde se encuentra el archivo WebApi.csproj), debería ver el mensaje Ahora escuchando: http://localhost:4000.
  4. De vuelta en la aplicación de ejemplo React 18 + Redux, elimine o comente las 2 líneas debajo del comentario // setup fake backend ubicado en /src/index.js archivo, luego inicie la aplicación React y ahora debería estar conectado con la API .NET.


Ejecute la aplicación React + Redux con una API de Node.js

Para obtener detalles completos sobre el ejemplo Node.js JWT Auth API, consulte la publicación NodeJS - JWT Authentication Tutorial with Example API. Pero para ponerse en marcha rápidamente, solo siga los pasos a continuación.

  1. Descargue o clone el código fuente del proyecto desde https://github.com/cornflourblue/node-jwt-authentication-api
  2. Inicie la API ejecutando npm start desde la línea de comando en la carpeta raíz del proyecto, debería ver el mensaje Server listening on port 4000.
  3. De vuelta en la aplicación de ejemplo React 18 + Redux, elimine o comente las 2 líneas debajo del comentario // setup fake backend ubicado en /src/index.js archivo, luego inicie la aplicación React y ahora debería estar conectado con la API de Node.js.


Estructura del proyecto React 18 + Redux

Create React App se usó para generar la estructura base del proyecto con el comando npx create-react-app <nombre del proyecto>, la herramienta también se usa para construir y servir la solicitud. Para obtener más información sobre Create React App, consulta https://create-react-app.dev/.

El código fuente del proyecto (/src) está organizado en las siguientes carpetas:

  • _components
    Componentes de React usados por páginas o por otros componentes de React.
  • _helpers
    Cualquier cosa que no encaje en las otras carpetas y no justifique tener su propia carpeta.
  • _store
    Redux store y slices que definen el estado global disponible para la aplicación React. Cada segmento contiene acciones y reductores que son responsables de actualizar el estado global. Para obtener más información sobre Redux, consulte https://redux.js.org.
  • home
    Componentes utilizados solo por la página de inicio
  • login
    Componentes utilizados solo por la página de inicio de sesión

Cada característica tiene su propia carpeta (home e login), otros códigos compartidos/comunes, como components, helpers, store, etc., se colocan en carpetas con el prefijo _ de subrayado para diferenciarlos fácilmente de funciones y agruparlas en la parte superior de la estructura de carpetas.

Los archivos de JavaScript están organizados con declaraciones export en la parte superior, por lo que es fácil ver todos los módulos exportados cuando abre un archivo. Las declaraciones de exportación van seguidas de funciones y otro código de implementación para cada módulo JS.

El archivo index.js en cada carpeta vuelve a exportar todos los módulos de esa carpeta para que puedan importarse usando solo la ruta de la carpeta en lugar de la ruta completa de cada módulo, y para habilitar importar varios módulos en una sola importación (por ejemplo, import { Nav, PrivateRoute } from '_components';). El archivo /_store/index.js también configura la tienda Redux centralizada que se proporciona a la aplicación React en el archivo index.js principal al iniciar.

El baseUrl se establece en "src" en el archivo jsconfig.json para hacer que todos declaraciones de importación (sin un punto '.' prefijo) relativas a la carpeta /src del proyecto, eliminando la necesidad de rutas relativas largas como import { history } from '../../../_helpers';.

Haga clic en cualquiera de los enlaces a continuación para saltar a una descripción de cada archivo junto con su código:

 

Archivo index.html principal

Ruta: /public/index.html

El archivo index.html principal es la página inicial cargada por el navegador que inicia todo. Crear aplicación React (con Webpack bajo el capó) agrupa todos los archivos javascript compilados y los inyecta en el cuerpo de la página index.html para que el navegador pueda cargar y ejecutar los scripts.

<!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 - JWT Authentication Example</title>

    <!-- bootstrap css -->
    <link href="//netdna.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
</body>
</html>
 

Componente de navegación

Ruta: /src/_components/Nav.jsx

El componente de navegación muestra la barra principal en el ejemplo. El componente obtiene el authUser actual del estado global de Redux con el enlace useSelector() y solo muestra la navegación si el usuario ha iniciado sesión.

El componente del enrutador de reacción NavLink agrega automáticamente la clase active al elemento de navegación activo para que se resalte en la interfaz de usuario.

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">
            <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>
    );
}
 

Ruta privada

Ruta: /src/_components/PrivateRoute.jsx

El componente de ruta privada de reacción representa componentes secundarios (child) si el usuario ha iniciado sesión. Si no ha iniciado sesión, el usuario es redirigido a la página /login con el URL de retorno pasada en la ubicación state propiedad.

El usuario conectado actual (authUser) se recupera de Redux con una llamada al gancho useSelector().

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;
}
 

Backend falso

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

Para ejecutar y probar la aplicación React + Redux sin una API de back-end real, el ejemplo utiliza un back-end falso que intercepta las solicitudes HTTP de la aplicación React y envía mensajes "falsos" respuestas. Esto se hace mediante parches de mono (monkey patching) en la función window.fetch() para devolver respuestas falsas para un conjunto específico de rutas.

Monkey patching

Monkey patching es una técnica utilizada para alterar el comportamiento de una función existente, ya sea para extenderla o cambiar la forma en que funciona. En JavaScript, esto se hace almacenando una referencia a la función original en una variable y reemplazando la función original con una nueva función personalizada que (opcionalmente) llama a la función original antes/después de ejecutar algún código personalizado.

El backend falso está organizado en una función handleRoute() de nivel superior que verifica la URL y el método de la solicitud para determinar cómo se debe manejar la solicitud. Para rutas falsas, se llama a una de las siguientes // route functions, para todas las demás rutas, la solicitud se pasa al backend real llamando a la función de solicitud de búsqueda original (realFetch(url, opts)). Debajo de las funciones de ruta hay // helper functions para devolver diferentes tipos de respuesta y realizar tareas pequeñas.

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,
                    token: 'fake-jwt-token'
                });
            }

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

            // helper functions

            function ok(body) {
                resolve({ ok: true, text: () => Promise.resolve(JSON.stringify(body)) })
            }

            function unauthorized() {
                resolve({ status: 401, text: () => Promise.resolve(JSON.stringify({ message: 'Unauthorized' })) })
            }

            function error(message) {
                resolve({ status: 400, text: () => Promise.resolve(JSON.stringify({ message })) })
            }

            function isAuthenticated() {
                return opts.headers['Authorization'] === 'Bearer fake-jwt-token';
            }

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

Contenedor fetch

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

El contenedor fetch es un contenedor ligero alrededor de la función fetch() del navegador nativo que se utiliza para simplificar el código para realizar solicitudes HTTP. Devuelve un objeto con métodos para solicitudes get, post, put y delete, maneja automáticamente el análisis de Datos JSON de las respuestas y arroja un error si la respuesta HTTP no es exitosa (!response.ok). Si la respuesta es 401 No autorizado o 403 Prohibido, el usuario se desconecta automáticamente de la aplicación React + Redux.

La función authHeader() se usa para agregar automáticamente un token de autenticación JWT al encabezado de autorización HTTP de la solicitud si el usuario inició sesión y la solicitud es a la URL de la API de la aplicación.

La función authToken() devuelve el token JWT para el usuario conectado actual, o nulo si no ha iniciado sesión. El token se recupera de Redux usando store.getState() en lugar del enlace useSelector() porque las funciones de enlace solo se pueden llamar desde los componentes de React u otras funciones de enlace.

Con el envoltorio de búsqueda, se puede realizar una solicitud POST de la siguiente manera: fetchWrapper.post(url, body);. Se usa en la aplicación de ejemplo en Redux auth slice y users slice.

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 jwt if user is logged in and request is to the api url
    const token = authToken();
    const isLoggedIn = !!token;
    const isApiUrl = url.startsWith(process.env.REACT_APP_API_URL);
    if (isLoggedIn && isApiUrl) {
        return { Authorization: `Bearer ${token}` };
    } else {
        return {};
    }
}

function authToken() {
    return store.getState().auth.user?.token;
}

function handleResponse(response) {
    return response.text().then(text => {
        const data = text && JSON.parse(text);

        if (!response.ok) {
            if ([401, 403].includes(response.status) && authToken()) {
                // auto logout if 401 Unauthorized or 403 Forbidden response returned from api
                const logout = () => store.dispatch(authActions.logout());
                logout();
            }

            const error = (data && data.message) || response.statusText;
            return Promise.reject(error);
        }

        return data;
    });
}
 

Historia

Ruta: /src/_helpers/history.js

El ayudante de historial es un objeto javascript simple que brinda acceso a la función navigate() del enrutador React y a la propiedad location desde cualquier lugar de la aplicación React, incluidos los componentes externos. En este ejemplo, es necesario habilitar la navegación al iniciar y cerrar sesión desde Redux auth slice. Las propiedades se inicializan al iniciar la aplicación en el componente de la aplicación raíz con el enrutador React hooks useNavigate() y useLocation().

De manera predeterminada, solo puede navegar desde el interior de los componentes de React o funciones de enlace con el enlace useNavigate(). Hay un HistoryRouter disponible en React Router 6 que acepta un objeto history personalizado para permitir la navegación fuera de los componentes. Sin embargo (en el momento de escribir este artículo), el componente HistoryRouter tiene el prefijo unstable_ porque todavía tiene algunos problemas, así que decidí optar por una solución alternativa.

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

Redux auth slice

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

La porción de autenticación administra el estado, las acciones y los reductores de Redux para la autenticación. El archivo está organizado en tres secciones para que sea más fácil ver lo que está pasando. La primera sección llama a funciones para crear y configurar el segmento, la segunda sección exporta las acciones y el reductor, y la tercera sección contiene las funciones que implementan la lógica.

initialState define las propiedades de estado en el segmento con sus valores iniciales. La propiedad de estado user contiene el usuario conectado actual, se inicializa con el objeto 'user' del almacenamiento local para permitir permanecer conectado entre actualizaciones de página y sesiones del navegador, o null si localStorage está vacío. El error se muestra en el componente de inicio de sesión si falla el inicio de sesión.

El objeto reducers pasado a createSlice() contiene lógica para acciones sincrónicas (cosas que no tiene que esperar) . Por ejemplo, el reductor logout establece la propiedad de estado user en nulo, lo elimina del almacenamiento local y lo redirige a la página de inicio de sesión. No realiza ninguna tarea asincrónica, como una solicitud de API. La función createSlice() genera automáticamente acciones coincidentes para estos reductores y las expone a través de la propiedad slice.actions.

Acciones asíncronas con createAsyncThunk()

El objeto extraActions contiene lógica para acciones asincrónicas (cosas por las que debe esperar), como solicitudes de API. Las acciones asíncronas se crean con la función createAsyncThunk() de Redux Toolkit. El objeto extraReducers contiene métodos para actualizar el estado de Redux en diferentes etapas de acciones asíncronas (pending, fulfilled, rejected) , y se pasa como parámetro a la función createSlice().

El método de acción login() publica credenciales en la API, en caso de éxito (fulfilled), el objeto de usuario devuelto se almacena en el estado Redux user prop y localStorage, y el usuario es redirigido a la URL de retorno o página de inicio. En caso de falla (rejected), el error se almacena en la propiedad error del estado de Redux que se muestra dentro del componente de inicio de sesión.

Exportar acciones y reductor para Redux Slice

La exportación de authActions incluye todas las acciones de sincronización (slice.actions) y las acciones asíncronas (extraActions) para el segmento de autenticación.

El reductor para el segmento de autenticación se exporta como authReducer, que se usa en la tienda Redux para que la aplicación lo configure la tienda de estado global.

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 {
        ...login()
    };

    function login() {
        var { pending, fulfilled, rejected } = extraActions.login;
        return {
            [pending]: (state) => {
                state.error = null;
            },
            [fulfilled]: (state, action) => {
                const user = action.payload;
                
                // store user details and jwt token 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);
            },
            [rejected]: (state, action) => {
                state.error = action.error;
            }
        };
    }
}
 

Redux users slice

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

La porción de usuarios administra el estado Redux, las acciones y los reductores para los usuarios en la aplicación React. Cada parte del segmento está organizada en su propia función que se llama desde la parte superior del archivo para que sea más fácil ver lo que está pasando. initialState define las propiedades de estado en este segmento con sus valores iniciales. La propiedad users se utiliza para almacenar todos los usuarios obtenidos de la API. Por defecto es un objeto vacío y puede contener uno de los siguientes valores:

  • {} - estado inicial.
  • { loading: true }: los usuarios se obtienen actualmente de la API.
  • [{ ... }, { ... }, { ... }] - matriz de usuarios devueltos por la API.
  • { error: { message: 'mensaje de error' } }: la solicitud a la API falló y se devolvió un error.

Acciones asíncronas con createAsyncThunk()

El objeto extraActions contiene lógica para acciones asincrónicas (cosas por las que debe esperar), como solicitudes de API. Las acciones asíncronas se crean con la función createAsyncThunk() de Redux Toolkit. El objeto extraReducers contiene métodos para actualizar el estado de Redux en diferentes etapas de acciones asíncronas (pending, fulfilled, rejected) , y se pasa como parámetro a la función createSlice().

El método de acción getAll() obtiene los usuarios de la API, en caso de éxito (fulfilled) la matriz de usuarios devueltos (action.payload) se almacena en la propiedad users del estado de Redux.

Exportar acciones y reductor para Redux Slice

La exportación de userActions incluye todas las acciones de sincronización (slice.actions) y las acciones asíncronas (extraActions) para el segmento de usuarios.

El reductor para el segmento de usuarios se exporta como usersReducer, que se usa en el almacén Redux raíz para configurar el estado global para la aplicación React.

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 {
        ...getAll()
    };

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

Almacén Redux

Ruta: /src/_store/index.js

El archivo de índice del almacén configura el almacén raíz de Redux para la aplicación React con la función configureStore(). El almacén de Redux devuelto contiene las propiedades de estado auth y users que se asignan a sus segmentos correspondientes.

El archivo de índice también vuelve a exportar todos los módulos de los segmentos de Redux en la carpeta. Esto permite que los módulos de Redux se importen directamente desde la carpeta _store sin la ruta al archivo de segmento. También permite realizar importaciones múltiples desde diferentes archivos a la vez (por ejemplo, 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
    },
});
 

Componente de inicio

Ruta: /src/home/Home.jsx

La página de inicio se muestra después de iniciar sesión en la aplicación, muestra el nombre del usuario que inició sesión más una lista de todos los usuarios en la aplicación del tutorial. Los usuarios se cargan en el estado de Redux llamando a dispatch(userActions.getAll()); desde la función de gancho useEffect(), vea cómo se obtienen los usuarios y el estado de Redux se actualiza en segmento de usuarios.

La lista de usuarios se muestra si la propiedad de estado users contiene una matriz con al menos 1 elemento, lo que se confirma comprobando {users.length && ...}.

Se muestra una rueda giratoria de carga mientras la solicitud de la API para los usuarios está en curso o se está cargando, y se muestra un mensaje de error si la solicitud falla.

Los valores de estado de Redux se recuperan para los datos auth y users con la ayuda de la función de enlace useSelector().

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 & JWT!!</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>
    );
}
 

Componente de inicio de sesión

Ruta: /src/login/Login.jsx

La página de inicio de sesión contiene un formulario creado con la biblioteca React Hook Form que contiene campos de nombre de usuario y contraseña para iniciar sesión en la aplicación React + Redux.

Las reglas de validación de formularios se definen con la biblioteca de validación de esquemas Yup y se pasan con formOptions a la función React Hook Form useForm(), para obtener más información sobre Yup, consulte https://github.com/jquense/yup.

La función gancho useForm() devuelve un objeto con métodos para trabajar con un formulario, incluido el registro de entradas, el manejo del envío de formularios, el acceso al estado del formulario, la visualización de errores y más. Para obtener una lista completa, consulte https://react-hook-form.com/api/useform.

La función onSubmit se llama cuando el formulario se envía y es válido, y envía las credenciales de usuario a la API llamando a dispatch(authActions.login({ nombre de usuario, contraseña })). En una autenticación exitosa, los datos del usuario con el token JWT se almacenan en el estado compartido de Redux mediante el reductor login.fulfilled en el auth slice.

La plantilla JSX devuelta contiene el marcado de la página, incluido el formulario, los campos de entrada y los mensajes de validación. Los campos de formulario se registran con React Hook Form llamando a la función de registro con el nombre de campo de cada elemento de entrada (por ejemplo, {...register('username')}). Para obtener más información sobre la validación de formularios con React Hook Form, consulte React Hook Form 7 - Ejemplo de validación de formulario.

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="form-group">
                            <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="form-group">
                            <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 mr-1"></span>}
                            Login
                        </button>
                        {authError &&
                            <div className="alert alert-danger mt-3 mb-0">{authError.message}</div>
                        }
                    </form>
                </div>
            </div>
        </div>
    )
}
 

Componente de aplicación

Ruta: /src/App.jsx

El componente App es el componente raíz de la aplicación de ejemplo, contiene el html externo, la navegación principal y las rutas de la aplicación.

La ruta /login es pública y la ruta principal (/) está protegida por la ruta privada componente que utiliza Redux para comprobar si el usuario ha iniciado sesión.

La última ruta es una ruta de redirección general que redirige cualquier ruta no coincidente a la página de inicio.

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>
    );
}
 

Estilos CSS globales

Ruta: /src/index.css

El archivo de hoja de estilo global contiene estilos CSS que se aplican globalmente en toda la aplicación React, se importa en el archivo index.js principal a continuación.

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

Archivo index.js principal

Ruta: /src/index.js

El archivo principal index.js arranca la aplicación React + Redux representando el componente App en el elemento div root ubicado en el archivo html de índice principal.

El componente Provider es el proveedor de contexto para el estado de Redux y es un ancestro requerido para cualquier componente de React que acceda al estado de Redux. Envolverlo alrededor del componente raíz App hace que Redux store sea global, por lo que es accesible para todos los componentes en la aplicación React.

El componente React.StrictMode no representa ningún elemento en la interfaz de usuario, se ejecuta en modo de desarrollo para resaltar posibles problemas/errores en la aplicación React. Para obtener más información, consulta https://reactjs.org/docs/strict-mode.html.

BrowserRouter agrega compatibilidad con las características de Routes y React Router 6 desde cualquier componente de la aplicación.

Antes de iniciar la aplicación React, se importa la hoja de estilo CSS global (./index.css) y la API backend falso está habilitada. Para deshabilitar el backend falso, simplemente elimine o comente las 2 líneas debajo del comentario // 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

Ruta: /.env

El archivo dotenv contiene variables de entorno utilizadas en la aplicación React de ejemplo, la URL de la API se utiliza en Redux auth slice y users slice para enviar solicitudes HTTP a la API.

Las variables de entorno configuradas en el archivo dotenv que tienen el prefijo REACT_APP_ son accesibles en la aplicación React a través de process.env.<variable name> (por ejemplo, proceso.env.REACT_APP_API_URL). Para obtener más información sobre el uso de variables de entorno en React, consulte https://create-react-app.dev/docs/adding-custom-environment-variables/

REACT_APP_API_URL=http://localhost:4000
 

jsconfig.json

Ruta: /jsconfig.json

La siguiente configuración habilita la compatibilidad con importaciones absolutas a la aplicación, por lo que los módulos se pueden importar con rutas absolutas en lugar de rutas relativas (p. ej., import { MyComponent } from '_components'; en lugar de import { MyComponent } from '../../../_components';).

Para obtener más información sobre importaciones absolutas en React, consulte https://create-react-app.dev/docs/importing-a-component/#absolute-imports.

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

Package.json

Ruta: /paquete.json

El archivo package.json contiene información de configuración del proyecto, incluidas las dependencias del paquete que se instalan cuando ejecuta npm install y scripts que se ejecutan cuando ejecuta npm start o npm run build etc. La documentación completa está disponible en https://docs.npmjs.com/files/package.json.

{
    "name": "react-18-redux-jwt-authentication-example",
    "version": "0.1.0",
    "private": true,
    "dependencies": {
        "@hookform/resolvers": "^2.9.0",
        "@reduxjs/toolkit": "^1.8.2",
        "react": "^18.1.0",
        "react-dom": "^18.1.0",
        "react-hook-form": "^7.31.3",
        "react-redux": "^8.0.2",
        "react-router-dom": "^6.3.0",
        "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"
        ]
    }
}

 


Suscríbete o Sígueme para actualizaciones

Suscríbete a mi canal de YouTube o sígueme en Twitter, Facebook o GitHub para recibir notificaciones cuando publique contenido nuevo.

Aparte de la codificación...

Actualmente estoy intentando viajar por Australia en motocicleta con mi esposa Tina en un par de Royal Enfield Himalayan. Puedes seguir nuestras aventuras en YouTube, Instagram y Facebook.


¿Necesita Ayuda React?

Buscar fiverr para encontrar ayuda rápidamente de desarrolladores React experimentados.