使用React 18.1.0、Redux 4.2.0和Redux 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存储包含状态属性auth 和users ,这些属性映射到它们相应的片断。
索引文件还重新导出了文件夹中的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>
)
}