React + Redux - 使用createAsyncThunk的异步行动中的HTTP POST请求

2,311 阅读3分钟

使用React 18.1.0Redux 4.2.0Redux Toolkit 1.8.2构建的教程

这是一个快速的例子,说明如何在Redux中使用Redux工具包的createAsyncThunk() 函数创建的异步动作向API发送HTTP POST请求。

下面的代码片段展示了如何使用Redux动作将登录凭证从React组件中的表单发送到API,并根据结果执行不同的逻辑 - 成功或失败。这些代码来自于我最近发布的React + Redux JWT认证教程,其中包括一个实时演示,要查看完整的代码和运行实例,请查看React 18 + Redux - JWT认证实例和教程

Redux Auth Slice

路径。/src/_store/auth.slice.js

auth slice管理Redux状态、动作和认证的还原器。该文件被组织成三个部分,以使其更容易看到正在发生的事情。第一部分调用函数来创建和配置分片,第二部分导出动作和还原器,第三部分包含实现逻辑的函数。

initialState 定义了切片中的状态属性和它们的初始值。 状态属性持有当前登录的用户,它被初始化为来自本地存储的 对象,以支持在页面刷新和浏览器会话之间保持登录状态,如果 localStorage 为空,则 。如果登录失败, ,显示在user 'user' null error 登录组件中。

传递给createSlice()reducers 对象包含同步动作的逻辑(你不必等待的事情)。例如,logout 还原器将user 状态属性设置为空,将其从本地存储中删除,并重定向到登录页面。它不执行任何异步任务,如API请求。createSlice() 函数为这些还原器自动生成匹配的动作,并通过slice.actions 属性公开它们。

异步行动与createAsyncThunk()

extraActions 对象包含异步行动(你必须等待的事情)的逻辑,如API请求。异步动作是通过Redux工具包createAsyncThunk() 函数创建的。createAsyncThunk的第一个参数是动作的名称,Redux动作名称的标准惯例是'[slice name]/[action name]' ,例如('auth/login')。第二个参数是执行该动作的异步函数,完成后返回结果。

对于用createAsyncThunk() 创建的每个异步动作,Redux工具包会自动生成三个Redux动作,异步动作的每个阶段都有一个。pending,fulfilled,rejected

extraReducers 对象包含在createAsyncThunk() 生成的三个不同阶段的异步行动中的每个阶段更新 Redux 状态的方法,以及作为参数传递给createSlice() 函数的对象,以在 Redux 片段中包含额外的还原器。

登录动作方法的HTTP POST

login() 动作方法将凭证发布到API,在成功时(fulfilled),返回的用户对象被存储在Redux状态user prop和localStorage中,用户被重定向到返回url或主页。在失败时(rejected),错误被存储在Redux状态error 属性中,由登录组件呈现。

Redux slice的导出动作和还原器

authActions 输出包括所有同步动作 (slice.actions) 和异步动作 (extraActions) ,用于 auth slice。

auth分片的还原器被导出为authReducer ,它被用于Redux商店的应用,以配置全局状态存储。

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商店

路径。/src/_store/index.js

商店索引文件通过configureStore() 函数为React应用程序配置根Redux商店。返回的Redux存储包含状态属性authusers ,这些属性映射到它们相应的片断。

索引文件还重新导出了文件夹中的Redux切片的所有模块。这使得Redux模块可以直接从_store 文件夹中导入,而无需切片文件的路径。它还可以一次从不同的文件中导入多个模块(例如: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
    },
});

React登录组件

路径。/src/login/Login.jsx

登录页面包含一个用React Hook Form库构建的表单,其中包含用户名和密码字段,用于登录到React + Redux应用。

表单验证规则是用Yup模式验证库定义的,并与formOptions 一起传递给React Hook FormuseForm() 函数,关于Yup的更多信息见github.com/jquense/yup…

useForm() 钩子函数返回一个对象,该对象具有处理表单的方法,包括注册输入、处理表单提交、访问表单状态、显示错误等,完整列表见react-hook-form.com/api/useform…

Redux中来自登录表单的HTTP POST请求

当表单被提交且有效时,onSubmit 函数会被调用,并通过调用dispatch(authActions.login({ username, password })) ,在HTTP POST请求中发送用户证书到API。dispatch() 函数将异步login 动作方法分配给Redux商店。

认证成功后,用户数据(包括JWT令牌)被login.fulfilled reducer保存在Redux共享状态中,并且用户被重定向到主页。

在失败时,错误信息由login.rejected reducer保存在Redux状态中。该错误会自动在登录表单的底部呈现为一个警告({authError.message})。

返回的JSX模板包含页面的标记,包括表单、输入字段和验证信息。表单字段通过调用register函数和每个输入元素的字段名在React Hook Form中注册(例如:{...register('username')} )。关于用React Hook Form进行表单验证的更多信息,请看React Hook Form 7 - 表单验证实例

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