Publicada:

React + Redux - Solicitud HTTP POST en acción asíncrona con createAsyncThunk

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

Este es un ejemplo rápido de cómo enviar una solicitud HTTP POST a una API en Redux usando una acción asíncrona creada con la función createAsyncThunk() de Redux Toolkit.

Los fragmentos de código a continuación muestran cómo publicar las credenciales de inicio de sesión desde un formulario en un componente React a una API usando una acción Redux, y ejecutar una lógica diferente según el resultado: éxito o fracaso. Son de un tutorial de autenticación de React + Redux JWT que publiqué recientemente que incluye una demostración en vivo, para ver el código completo y el ejemplo en ejecución, consulte React 18 + Redux - Ejemplo y tutorial de autenticación JWT.

 

Segmento de autenticación de Redux

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 primer parámetro para createAsyncThunk es el nombre de la acción, la convención estándar para los nombres de acción de Redux es '[slice name]/[action name]' p. ('auth/login'). El segundo parámetro es la función asíncrona que realiza la acción y devuelve el resultado cuando finaliza.

Para cada acción asíncrona creada con createAsyncThunk(), el kit de herramientas de Redux genera automáticamente tres acciones Redux, una para cada etapa de la acción asíncrona: pending, fulfilled, rejected.

El objeto extraReducers contiene métodos para actualizar el estado de Redux en cada una de las tres etapas diferentes de las acciones asincrónicas generadas por createAsyncThunk(), y el objeto se pasa como parámetro a la función createSlice() para incluir los reductores adicionales en el segmento de Redux.

HTTP POST del método de acción de inicio de sesión

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 error (rejected), el error se almacena en la propiedad error del estado de Redux que representa el 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 el almacén Redux para que la aplicación lo configure el almacén estatal 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;
            }
        };
    }
}
 

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 de sesión de React

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.

Solicitud HTTP POST en Redux desde el formulario de inicio de sesión

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 en una solicitud HTTP POST llamando a dispatch(authActions.login({ username, password })). La función dispatch() envía el método de acción asíncrono login al almacén de Redux.

En una autenticación exitosa, los datos del usuario (incluido el token JWT) se guardan en el estado compartido de Redux mediante el reductor login.fulfilled en el segmento de autenticación, y el usuario es redirigido a la página de inicio.

En caso de falla, el reductor login.rejected guarda el mensaje de error en estado Redux. El error se representa automáticamente como una alerta en la parte inferior del formulario de inicio de sesión ({authError.message}).

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


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.