Redxu(RTK) 基础 异步逻辑与数据请求 第5.3节 加载用户数据
加载用户数据
现在页面上关于作者的信息是 unknown,因为我们没有正确的拉取user信息吖。
怎么做捏?太简单了,前面已经拉取了posts,现在拉取user就行了!干就完了,加油奥利给。
在userSlice里写thunk。
//这是注释,显示文件路径捏:/src/features/users/usersSlice.ts
import { createAsyncThunk } from "@reduxjs/toolkit";
import { createSlice } from "@reduxjs/toolkit";
import { client } from "../../api/client";
interface IState {
users: IUser[];
status: "idle" | "pending" | "success" | "failed";
error: null | any;
}
type IUser = {
id: string;
name: string;
};
const initialState: IState = {
status: "idle",
error: null,
users: [],
};
export const fetchUsers = createAsyncThunk("users/fecthUsers", async () => {
const response = await client.get("/fakeApi/users");
return response.data;
});
const usersSlice = createSlice({
name: "users",
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(fetchUsers.fulfilled, (state, action) => {
console.log(action.payload);
state.users = action.payload;
});
},
});
export default usersSlice.reducer;
跟文档有点不同,文档在extraReducers 用了另一种写法 ,不过这没啥问题。
builder.addCase(fetchUsers.fulfilled, (state, action) => {
return action.payload
})
}
然后到App.tsx里边拉取user数据,只用在app启动前拉取一次就行了。
function App() {
const dispatch = useAppDispatch();
dispatch(fetchUsers());
// 省略其他内容捏!
现在页面上的作者应该已经能够正常显示了捏!
添加新帖子
到第五节的最后一个小节了。
当我们点击新增post的时候,数据还是存在内存里,因为有模拟的后端了,我们要进行API调用,把数据存到数据库里,当然要注意,因为是mock的后端,所以刷新页面后新增数据不会持久化,但是我们的核心在用mock后端进行异步的post请求,下面进行具体操作捏!
使用thunk发送新帖子的数据
首先还是写个thunk,如下:
注意 initialPost参数实际上就是从客户端接收到的表单数据,post到服务器后,后端要返回带有id等其他数据的完整的post数据。
export const addNewPost = createAsyncThunk(
"posts/addNewPost",
// payload 创建者接收部分“{title, content, user}”对象
async (initialPost: Pick<IPost, "title" | "user" | "content">) => {
// 我们发送初始数据到 API server
const response = await client.post("/fakeApi/posts", initialPost);
// 响应包括完整的帖子对象,包括唯一 ID
return response.data;
}
);
检查组件中的 Thunk 结果
在等待请求时可以禁用 “保存帖子” 按钮,那么用户就不会意外地尝试保存帖子两次,这样交互会更友好。如果请求失败,我们可能还想在表单中显示错误消息,或者只是将其记录到控制台。
可以让我们的组件逻辑等待异步 thunk 完成,并在完成后检查结果:
上面内容意思就是,我们要跟踪thunk的状态,只在thunk真正fulfilled的时候才能更新数据捏。
额,下面有点多,其实就是更新了addPost这个thunk的使用,另外根据thunk实际请求状态来判断是否请求成功,如果还在请求,那么保存按钮是不可用的。
//这是注释,显示文件路径捏:/src/features/posts/AddPostForm.tsx
import React, { useState } from "react";
import { nanoid } from "@reduxjs/toolkit";
import { useAppDispatch, useAppSelector } from "../../app/hooks";
import { addNewPost, postAdd } from "./postsSlice";
export const AddPostForm = () => {
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
const dispatch = useAppDispatch();
const [userId, setUserId] = useState("");
const [addRequestStatus, setAddRequestStatus] = useState("idle");
const users = useAppSelector((state) => state.users);
const onTitleChanged = (e: React.ChangeEvent<HTMLInputElement>) =>
setTitle(e.target.value);
const onContentChanged = (e: React.ChangeEvent<HTMLTextAreaElement>) =>
setContent(e.target.value);
const onAuthorChanged = (e: React.ChangeEvent<HTMLSelectElement>) =>
setUserId(e.target.value);
const canSave =
[title, content, userId].every(Boolean) && addRequestStatus === "idle";
const onSavePostClicked = async () => {
if (canSave) {
try {
setAddRequestStatus("pending");
await dispatch(addNewPost({ title, content, user: userId })).unwrap();
setTitle("");
setContent("");
setUserId("");
} catch (err) {
console.error("Failed to save the post: ", err);
} finally {
setAddRequestStatus("idle");
}
}
};
// 这是个 option 组件啊,造出来一个下拉菜单的效果
const usersOptions = users.users.map((user) => (
<option key={user.id} value={user.id}>
{user.name}
</option>
));
return (
<section>
<h2>添加新文章</h2>
<form>
<label htmlFor="postTitle">文章标题:</label>
<input
type="text"
id="postTitle"
name="postTitle"
value={title}
onChange={onTitleChanged}
/>
<label htmlFor="postAuthor">Author:</label>
<select id="postAuthor" value={userId} onChange={onAuthorChanged}>
<option value=""></option>
{usersOptions}
</select>
<label htmlFor="postContent">内容:</label>
<textarea
id="postContent"
name="postContent"
value={content}
onChange={onContentChanged}
/>
<button type="button" onClick={onSavePostClicked} disabled={!canSave}>
保存文章
</button>
</form>
</section>
);
};
当然,这个addNewPost对应的extraReducers我没写,你可以自己试一试。
可以使用 useState 添加一个表示加载状态的枚举状态,类似于我们在 postsSlice 中跟踪加载状态以获取帖子的方式。这样的话,就能知道请求是否在进行中。
当我们调用 dispatch(addNewPost()) 时,异步 thunk 从 dispatch 返回一个 Promise。我们可以在这里 await 那个 promise 知道 thunk 什么时候完成它的请求。但是,我们还不知道该请求是成功还是失败。
createAsyncThunk 在内部处理了所有错误,因此我们在日志中看不到任何关于“rejected Promises”的消息。然后它返回它 dispatch 的最终 action:如果成功则返回“已完成” action,如果失败则返回“拒绝” action。Redux Toolkit 有一个名为 unwrapResult 的工具函数,它将返回来自 fulfilled action 的 action.payload 数据,或者如果它是 rejected action 则抛出错误。这让我们可以使用正常的 try/catch 逻辑处理组件中的成功和失败。因此,如果帖子成功创建,我们将清除输入字段以重置表单,如果失败,则将错误记录到控制台。
但是,想要编写查看实际请求成功或失败的逻辑是很常见的。 Redux Toolkit 向返回的 Promise 添加了一个 .unwrap() 函数,它将返回一个新的 Promise,这个 Promise 在 fulfilled 状态时返回实际的 action.payload 值,或者在 “rejected” 状态下抛出错误。这让我们可以使用正常的“try/catch”逻辑处理组件中的成功和失败。 因此,如果帖子创建成功,我们将清除输入字段以重置表单,如果失败,则将错误记录到控制台。
如果你想查看 addNewPost API 调用失败时如何处理,请尝试创建一个新帖子,其中“内容”字段只有“error”一词(不含引号)。服务器将看到并发送回失败的响应,这样你应该会看到控制台记录一条日志。
总结
可以编写可复用的“selector 选择器”函数来封装从 Redux 状态中读取数据的逻辑
选择器是一种函数,它接收 Redux state 作为参数,并返回一些数据
Redux 使用叫做“ middleware ”这样的插件模式来开发异步逻辑
官方的处理异步 middleware 叫 redux-thunk,包含在 Redux Toolkit 中
Thunk 函数接收 dispatch 和getState 作为参数,并且可以在异步逻辑中使用它们
你可以 dispatch 其他 action 来帮助跟踪 API 调用的加载状态
典型的模式是在调用之前 dispatch 一个 "pending" 的 action,然后是包含数据的 “sucdess” 或包含错误的 “failure” action
加载状态通常应该使用枚举类型,如 'idle' | 'loading' | 'succeeded' | 'failed'
Redux Toolkit 有一个 createAsyncThunk API 可以为你 dispatch 这些 action
createAsyncThunk 接受一个 “payload creator” 回调函数,它应该返回一个 Promise,并自动生成 pending/fulfilled/rejected action 类型
像 fetchPosts 这样生成的 action creator 根据你返回的 Promise dispatch 这些 action
可以使用 extraReducers 字段在 createSlice 中监听这些 action,并根据这些 action 更新 reducer 中的状态。
action creator 可用于自动填充 extraReducers 对象的键,以便切片知道要监听的 action。
Thunk 可以返回 promise。 具体对于createAsyncThunk,你可以await dispatch(someThunk()).unwrap()来处理组件级别的请求成功或失败。