Redux文档七异步逻辑和数据获取
代码下载
异步逻辑和数据获取
介绍
在使用 Redux 数据 中,了解了如何在 React 组件内部使用来自 Redux Store 的多条数据,在分派操作对象之前自定义操作对象的内容,以及在 reducer 中处理更复杂的更新逻辑。
到目前为止,使用的所有数据都直接位于 React 客户端应用内部。然而,大多数实际应用需要通过调用 HTTP API 来获取和保存项目来处理来自服务器的数据。在本部分中,将转换社交媒体应用以从 API 获取帖子和用户数据,并通过将新帖子保存到 API 来添加新帖子。
提示:Redux 工具包包括 RTK 查询数据获取和缓存 API。RTK Query 是专门为 Redux 应用构建的数据获取和缓存的解决方案,无需编写任何额外的 Redux 逻辑(如 thunk 或 Reducer)来管理数据获取。RTK 查询作为数据获取的默认方法,本节了解数据获取如何与 Redux 协同工作的底层机制。
REST API 和客户端示例
为了保持示例项目的独立性和现实性,初始项目设置已经包含了一个用于数据的虚拟内存中 REST API(使用 Mock Service Worker 模拟 API 工具 配置)。该 API 使用 /fakeApi 作为端点的基本 URL,并支持 /fakeApi/posts、/fakeApi/users 和 fakeApi/notifications 的典型 GET/POST/PUT/DELETE HTTP 方法。它在 src/api/server.js 中定义。
该项目还包括一个小型 HTTP API 客户端对象,它公开 client.get() 和 client.post() 方法,类似于 axios 等流行的 HTTP 库。它在 src/api/client.js 中定义。
在这里将使用 client 对象对内存中的虚假 REST API 进行 HTTP 调用。
此外,模拟服务器已设置为每次加载页面时重用相同的随机种子,以便它将生成相同的假用户和假帖子列表。如果想重置该功能,删除浏览器本地存储中的 ‘randomTimestampSeed’ 值并重新加载页面,或者可以通过编辑 src/api/server.js 并将 useSeededRNG 设置为 false 来关闭该功能。
使用中间件启用异步逻辑
Redux Store 本身并不了解异步逻辑。它只知道如何同步分派操作,通过调用根 reducer 函数更新状态,并通知 UI 某些内容发生了变化。任何异步性都必须发生在存储之外。
如果想通过分派操作、检查当前 Store 状态或某种副作用来让异步逻辑与 Store 交互,该怎么办?这就是 Redux 中间件发挥作用的地方。它们扩展 Store 以添加其他功能,并允许:
- 分派任何操作时执行额外的逻辑(例如记录操作和状态)
- 暂停、修改、延迟、替换或停止已调度的操作
- 编写可以访问 dispatch 和 getState 的额外代码
- 教 dispatch 如何接受除普通操作对象之外的其他值,例如函数和 Promise,通过拦截它们并分派真实的操作对象
- 编写使用异步逻辑或其他副作用的代码
使用中间件的最常见原因是允许不同类型的异步逻辑与 Store 交互。这允许编写可以调度操作并检查 Store 状态的代码,同时将该逻辑与 UI 分开。
中间件和 Redux 数据流
中间件通过在 dispatch 的开头添加额外的步骤来更新 Redux 数据流。这样,中间件可以运行 HTTP 请求等逻辑,然后分派操作。这使得异步数据流看起来像这样:
thunks 和异步逻辑
Redux 有多种异步中间件,每种都允许使用不同的语法编写逻辑。最常见的异步中间件是 redux-thunk,它允许直接编写可能包含异步逻辑的普通函数。Redux Toolkit 的 configureStore 函数 默认自动设置 thunk 中间件,建议使用 thunk 作为使用 Redux 编写异步逻辑的标准方法。
thunk 函数
一旦 thunk 中间件被添加到 Redux 存储中,它就允许将 thunk 函数直接传递给 store.dispatch。thunk 函数将始终以 (dispatch, getState) 作为参数进行调用,可以根据需要在 thunk 中使用它们。
thunk 函数可以包含任何逻辑,同步或异步。Thunk 通常使用动作创建器来分派普通动作,例如 dispatch(increment()):
const store = configureStore({ reducer: counterReducer })
const exampleThunkFunction = (
dispatch: AppDispatch,
getState: () => RootState
) => {
const stateBefore = getState()
console.log(`Counter before: ${stateBefore.counter}`)
dispatch(increment())
const stateAfter = getState()
console.log(`Counter after: ${stateAfter.counter}`)
}
store.dispatch(exampleThunkFunction)
为了与分派正常操作对象保持一致,通常将它们编写为 thunk 操作创建者,它返回 thunk 函数。这些动作创建者可以采用可在 thunk 内部使用的参数。
const logAndAdd = (amount) => {
return (dispatch, getState) => {
const stateBefore = getState()
console.log('Counter before: ', stateBefore);
dispatch(incrementByAmount(amount))
const stateAfter = getState()
console.log('Counter after: ', stateAfter);
}
}
store.dispatch(logAndAdd(10))
Thunk 通常用 “slice” 文件 编写,因为 thunk 数据获取通常在概念上与特定切片的更新逻辑相关。
编写异步 thunk
Thunk 内部可能包含异步逻辑,例如 setTimeout、Promises 和 async/await。这使得它们成为放置对服务器 API 的 HTTP 调用的好地方。
Redux 的数据获取逻辑通常遵循可预测的模式:
- 在请求之前调度 “start” 操作,以指示请求正在进行中。这可用于跟踪加载状态以允许跳过重复请求或在 UI 中显示加载指示器。
- 异步请求是使用 fetch 或封装库发出的,得到 promise 结果。
- 当请求 promise 完成时,异步逻辑会分派包含结果数据的 “success” 操作或包含错误详细信息的 “failure” 操作。reducer 逻辑会清除两种情况下的加载状态,并处理成功案例的结果数据,或存储错误值以供潜在显示。
这些步骤不是必需的,但很常用。(如果只关心成功结果,则可以在请求完成时仅分派单个 “success” 操作,并跳过 “start” 和 “failure” 操作。)Redux Toolkit 提供了一个 createAsyncThunk API 来实现描述异步请求的操作的创建和调度。基本 createAsyncThunk 用法如下:
import { createAsyncThunk } from "@reduxjs/toolkit";
import { fetchCount } from "./counterAPI";
export const incrementAsync = createAsyncThunk('counter/fetchCount', async (amount) => {
const response = await fetchCount(amount)
return response.data
})
Redux Thunks
手写 Thunks
如果手动编写 thunk,则可以将 thunk 参数声明为 (dispatch, getState) :
const logAndAdd = (amount) => {
return (dispatch, getState) => {
const stateBefore = getState()
console.log('Counter before: ', stateBefore);
dispatch(incrementByAmount(amount))
const stateAfter = getState()
console.log('Counter after: ', stateAfter);
}
}
createAsyncThunk
见上文,略。
加载帖子
postsSlice 已经使用了一些硬编码的样本数据作为其初始状态。将把它切换为从空的帖子数组开始,然后从服务器获取帖子列表。必须更改 postsSlice 中的状态结构,以便我们可以跟踪 API 请求的当前状态。
请求的加载状态
当进行 API 调用时,可以将其进度视为一个小型状态机,该状态机可以处于四种可能状态之一:
- 请求尚未开始
- 请求正在进行中
- 请求成功,现在有了需要的数据
- 请求失败,可能有错误消息
可以使用一些布尔值(例如 isLoading: true)来跟踪该信息,但最好将这些状态作为单个枚举值来跟踪。一个好的模式是有一个如下所示的状态部分:
{
// 多个可能的状态值
status: 'idle', //'idle' | 'pending' | 'succeeded' | 'failed',
error: null// string | null
}
这些字段将与存储的任何实际数据一起存在。这些特定的字符串状态名称不是必需的,可以随意使用其他名称,例如 ‘loading’ 代替 ‘pending’,或 ‘completed’ 代替 ‘succeeded’。可以使用这些信息来决定在请求进行时在 UI 中显示什么内容,还可以在 reducer 中添加逻辑以防止诸如加载数据两次之类的情况。
新建 asyncPostsSlice.js 以使用此模式来跟踪 “获取帖子” 请求的加载状态。把状态从帖子数组本身切换为 {posts, status, error}。还将从初始状态中删除旧的示例帖子条目,并为加载和错误字段添加几个新的选择器:
import { createSlice, nanoid } from "@reduxjs/toolkit";
import { userLoggedOut } from "../auth/authSlice";
const initialReactions = {
thumbsUp: 0,
tada: 0,
heart: 0,
rocket: 0,
eyes: 0
}
const initialState = {
posts: [],
status: 'idle',
error: null
}
const asyncPostsSlice = createSlice({
name: 'asyncPosts',
initialState: initialState,
reducers: {
postAdded: {
reducer: (state, action) => {
state.posts.push(action.payload)
},
prepare: (title, content, userId) => {
return {payload: {id: nanoid(), title, content, user: userId,
date: new Date().toISOString(),
reactions: {...initialReactions}
}}
}
},
postUpdated: (state, action) => {
const {id, title, content} = action.payload
const existingPost = state.posts.find(post => post.id === id)
console.log('existingPost: ', existingPost);
if (existingPost) {
existingPost.title = title
existingPost.content = content
}
},
reactionAdded: (state, action) => {
const {postId, reaction} = action.payload
const existingPost = state.posts.find(post => post.id === postId)
console.log('existingPost: ', existingPost);
if (existingPost) {
existingPost.reactions[reaction]++
}
}
},
selectors: {
selectAllPosts: (postsState) => postsState.posts,
selectPostById: (postsState, postId) => postsState.posts.find(post => post.id === postId),
selectPostsStatus: (postsState) => postsState.status,
selectPostsError: (postsState) => postsState.error
},
extraReducers: (builder) => {
builder.addCase(userLoggedOut, (state) => {
return initialState
})
}
})
export const {selectAllPosts, selectPostById, selectPostsStatus, selectPostsError} = asyncPostsSlice.selectors
export const { postAdded, postUpdated, reactionAdded } = asyncPostsSlice.actions
export default asyncPostsSlice.reducer
作为更改的一部分,还需要将 state 作为数组的任何用途更改为 state.posts,因为数组现在更深一层。现在有一个 state.posts.posts 的嵌套对象路径,这有点重复和愚蠢,如果想避免这种情况,可以将嵌套数组名称更改为 items 或 data 或其他名称,但暂时保持原样。
使用 createAsyncThunk 获取数据
Redux Toolkit 的 createAsyncThunk API 会生成 thunk,自动分派 “开始/成功/失败” 操作。首先添加一个 thunk,它将发出 HTTP 请求以检索帖子列表。从 src/api 文件夹导入 client,并使用它向 /fakeApi/posts 发出请求。
import { createAsyncThunk } from "@reduxjs/toolkit";
import { client } from "../../api/client";
const fetchPosts = createAsyncThunk('posts/fetchPosts', async () => {
const response = await client.get('/fakeApi/posts')
return response.data
})
createAsyncThunk 接受两个参数:
- 将用作生成的操作类型的前缀的字符串
- “payload 创建者” 回调函数应该返回包含一些数据的 Promise,或者返回带有错误的被拒绝的 Promise
payload 创建者通常会发出某种 HTTP 请求,并且可以直接从 HTTP 请求返回 Promise,或者从 API 响应中提取一些数据并返回。通常使用 JS async/await 语法编写此代码,这可以编写使用 promise 的函数,同时使用标准 try/catch 逻辑而不是 somePromise.then() 链。
传入 asyncPosts/fetchPosts 作为操作类型前缀。fetchPosts 的 payload 创建回调不需要任何参数,它所需要做的就是等待 API 调用返回响应。响应对象类似 {data: []},希望调度的 Redux 操作具有只是帖子数组的 payload。因此提取 response.data,并从回调中返回它。
如果尝试调用 dispatch(fetchPosts()),fetchPosts thunk 将首先调度 asyncPosts/fetchPosts/pending 的操作类型,可以在 reducer 中监听此操作并将请求状态标记为 ‘pending’。一旦 Promise 解析,fetchPosts thunk 将获取从回调中返回的 response.data 数组,并调度包含帖子数组的 asyncPosts/fetchPosts/fulfilled 操作作为 action.payload。
reducer 和加载动作
接下来需要在 reducer 中处理这两个操作。这需要更深入地了解一直在使用的 createSlice API。createSlice 将在 reducers 字段中定义的每个 reducer 函数生成一个动作创建者,并且生成的动作类型包括切片的名称。
console.log(
postUpdated({ id: '123', title: 'First Post', content: 'Some text here' })
)
/*
{
type: 'asyncPosts/postUpdated',
payload: {
id: '123',
title: 'First Post',
content: 'Some text here'
}
}
*/
使用 createSlice 中的 extraReducers 字段用于响应在切片之外定义的操作。在这种情况下,需要监听 fetchPosts thunk 调度的 “pending” 和 “fulfilled” 操作类型。这些动作创建者附加到实际的 fetchPost 函数,可以将它们传递给 extraReducers 来监听这些动作:
extraReducers: (builder) => {
builder.addCase(userLoggedOut, (state) => {
return initialState
})
.addCase(fetchPosts.pending, (state, action) => {
state.status = 'pending'
})
.addCase(fetchPosts.fulfilled, (state, action) => {
state.status = 'succeeded'
state.posts.push(...action.payload)
})
.addCase(fetchPosts.rejected, (state, action) => {
state.status = 'failed'
state.error = action.error.message ?? 'Unknown Error'
})
}
根据返回的 Promise 处理 thunk 可以分派的所有三种操作类型:
- 当请求开始时,将 status 设置为 ‘pending’
- 如果请求成功,将 status 标记为 ‘succeeded’,并将获取的帖子添加到 state.posts
- 如果请求失败,会将 status 标记为 ‘failed’,并将任何错误消息保存到状态中,以便可以显示它
从组件中分派 thunk
已经编写了 fetchPosts thunk 并更新了切片来处理这些操作,重新构建 <AsyncPostsList> 组件以实际启动数据获取。
将 fetchPosts thunk 导入到组件中。与所有其他动作创建器一样,必须分派它,因此还需要添加 useDispatch 钩子。由于想在 <AsyncPostsList> 安装时获取此数据,需要导入 React useEffect 钩子并分派操作。
重要的是只尝试获取帖子列表一次。如果每次渲染 <AsyncPostsList> 组件时都执行此操作,或者由于在视图之间切换而重新创建组件,则最终可能会多次获取帖子。可以使用 posts.status 来帮助决定是否需要真正开始获取,方法是将其选入组件,并且仅在状态为 ‘idle’(即尚未开始)时才开始获取。
import { useSelector, useDispatch } from "react-redux";
import { fetchPosts, selectAllPosts, selectPostsStatus } from "./asyncPostsSlice";
import { useEffect } from "react";
export default function AsyncPostsList() {
const posts = useSelector(selectAllPosts)
const orderedPosts = posts.slice().sort((a, b) => b.date.localeCompare(a.date))
const dispatch = useDispatch()
const postStatus = useSelector(selectPostsStatus)
useEffect(() => {
if (postStatus === 'idle') {
dispatch(fetchPosts())
}
}, [postStatus, dispatch])
……
}
在 data.js 中添加路由数据后,看到一个新的帖子列表:
{
path: '/async/postsList',
title: '异步数据帖子列表示例',
element: <AsyncPostsList></AsyncPostsList>,
showHome: true
}
避免重复获取
遇到了一个问题,目前帖子列表显示每个帖子的重复项。这是为什么?刚刚添加了对 postStatus === 'idle' 的检查,这还不足以确保我们只分派一次 thunk 吗?
useEffect 中的实际逻辑是正确的。问题是,在开发中 React 将在其 <StrictMode> 组件内部运行两次所有 useEffect 钩子 是为了让某些类型的错误更明显地发生。
在这种情况下,发生的事情是:
- 已安装 组件
- useEffect 钩子首次运行。postStatus 值为 ‘idle’,因此它会分派 fetchPosts thunk。
- fetchPosts 立即分派其 fetchPosts.pending 操作,因此 Redux 存储确实立即将状态更新为 ‘pending’…
- 但 React 再次运行 useEffect 而没有重新渲染组件,因此仍然认为 postStatus 是 ‘idle’ 并第二次分派 fetchPosts
- 两个 thunk 都完成了数据获取,分派了
fetchPosts.fulfilled动作,fulfilled reducer运行了两次,从而导致一组重复的帖子被添加到状态中
那么,该如何解决这个问题?
- 一种选择是从我们的应用中删除
<StrictMode>标签。但是,React 团队建议使用它,它有助于发现其他问题。 - 可以使用 useRef 钩子编写一些复杂的逻辑来跟踪此组件是否实际上是第一次渲染,并使用它来仅分派一次 fetchPosts。但是,这有点丑陋。
- 最后一个选项是使用 Redux 状态中的实际 state.posts.status 值来检查是否已经有请求正在进行,如果是,则让 thunk 本身退出。幸运的是,createAsyncThunk 提供了一种方法来做到这一点。
检查异步 Thunk 条件
createAsyncThunk 接受可选的 condition 回调,可以使用它来进行该检查。如果提供,它将在 thunk 调用开始时运行,并且如果 condition 返回 false,它将取消整个 thunk
在这种情况下,如果 state.posts.status 字段不是 ‘idle’,想要避免运行 thunk。已经有一个可以在这里使用的 selectPostsStatus 选择器,因此可以添加 condition 选项并检查该值:
export const fetchPosts = createAsyncThunk('asyncPosts/fetchPosts', async () => {
console.log('asyncPosts');
const response = await client.get('/fakeApi/posts')
console.log('data: ', response.data);
return response.data
}, {
condition: (arg, thunkApi) => {
const postsStatus = selectPostsStatus(thunkApi.getState())
return postsStatus === 'idle'
}
})
重新加载页面并查看 <AsyncPostsList> 时,只看到一组帖子,没有重复,并且只在 Redux DevTools 中看到一组分派的操作。不需要将 condition 添加到所有 thunk,但有时确保一次只发出一个请求很有用。
显示加载状态
<AsyncPostsList> 组件已经在检查 Redux 中存储的帖子的任何更新,并在列表发生变化时重新渲染自身。因此,如果刷新页面,应该会看到来自虚假 API 的一组随机帖子出现在屏幕上。但是存在一些滞后 - <AsyncPostsList> 最初是空的,几秒钟后显示帖子。
真正的 API 调用可能需要一些时间才能返回响应,因此通常最好在 UI 中显示某种 “正在加载…” 指示器,以便用户知道正在等待数据。可以更新 <AsyncPostsList> 以根据 state.posts.status 枚举显示不同的 UI:如果正在加载,则显示一个加载控件;如果失败,则显示一条错误消息;如果有数据,则显示实际的帖子列表。
此时提取 <PostExcerpt> 组件来封装列表中一项的渲染,结果如下所示:
import { useSelector, useDispatch } from "react-redux";
import { Link } from "react-router-dom";
import { fetchPosts, selectAllPosts, selectPostsStatus, selectPostsError } from "./asyncPostsSlice";
import PostAuthor from "../posts/PostAuthor";
import AsyncReactionButtons from "./AsyncReactionButtons";
import { useEffect } from "react";
import Spinner from "../../components/Spinner";
function PostExcerpt({post}) {
return <article>
<h3><Link to={`/async/posts/${post.id}`}>{post.title}</Link></h3>
<p>{post.content.substring(0, 100)}</p>
<PostAuthor userId={post.user}></PostAuthor>
<AsyncReactionButtons post={post}></AsyncReactionButtons>
</article>
}
export default function AsyncPostsList() {
const dispatch = useDispatch()
const postStatus = useSelector(selectPostsStatus)
useEffect(() => {
if (postStatus === 'idle') {
dispatch(fetchPosts())
}
}, [postStatus, dispatch])
const posts = useSelector(selectAllPosts)
const postsError= useSelector(selectPostsError)
let content = null
if (postStatus === 'pending') {
content = <Spinner text='Loading...'></Spinner>
} else if (postStatus === 'succeeded') {
const orderedPosts = posts.slice().sort((a, b) => b.date.localeCompare(a.date))
content = orderedPosts.map((post) => <PostExcerpt key={post.id} post={post}>
</PostExcerpt>)
} else if (postStatus === 'rejected') {
content = <div>{postsError}</div>
}
return (<section>
<h2>Posts</h2>
{content}
</section>)
}
可能会注意到 API 调用需要一段时间才能完成,并且加载控件会在屏幕上停留几秒钟。模拟 API 服务器配置为向所有响应添加 2 秒的延迟,如果想改变这个行为,可以打开 api/server.js,并改变这一行:
const ARTIFICIAL_DELAY_MS = 2000
可选:在 createSlice 中定义 Thunk
现在 fetchPosts thunk 在 postsSlice.js 文件中定义,但在 createSlice() 调用之外。有一种可选的方法可以在 createSlice 内部定义 thunk,这需要更改 reducers 字段的定义方式。
编写 createSlice.reducers 字段的标准方法是将其作为对象,其中键成为操作名称,值是 Reducer。值还可以是具有 {reducer, prepare} 函数的对象,用于创建具有想要的值的操作对象。
或者,reducers 字段可以是接收 create 对象的回调函数。这与使用 extraReducers 看到的有些相似,但创建 Reducer 和操作的方法不同:
- create.reducer(caseReducer):定义 case reducer
- create.preparedReducer(prepare, caseReducer):使用准备回调定义 Reducer
像以前一样返回一个以 Reducer 名称作为字段的对象,但调用 create 方法来制作每个 Reducer。将 postsSlice 转换为此语法后如下所示:
const asyncPostsSlice = createSlice({
name: 'asyncPosts',
initialState: initialState,
reducers: (create) => {
return {
postAdded: create.preparedReducer((title, content, userId) => {
return {payload: {id: nanoid(), title, content, user: userId,
date: new Date().toISOString(),
reactions: {...initialReactions}
}}
}, (state, action) => {
state.posts.push(action.payload)
}),
postUpdated: create.reducer((state, action) => {
const {id, title, content} = action.payload
const existingPost = state.posts.find(post => post.id === id)
console.log('existingPost: ', existingPost);
if (existingPost) {
existingPost.title = title
existingPost.content = content
}
}),
reactionAdded: create.reducer((state, action) => {
const {postId, reaction} = action.payload
const existingPost = state.posts.find(post => post.id === postId)
console.log('existingPost: ', existingPost);
if (existingPost) {
existingPost.reactions[reaction]++
}
})
}
},
……
})
将 reducers 编写为回调为扩展 createSlice 的功能打开了大门。特别是,可以制作一个特殊版本的 createSlice,该版本能够使用内置的 createAsyncThunk。导入 buildCreateSlice 和 asyncThunkCreator,然后像这样调用 buildCreateSlice:
import { buildCreateSlice, asyncThunkCreator } from '@reduxjs/toolkit'
export const createAppSlice = buildCreateSlice({
creators: { asyncThunk: asyncThunkCreator }
})
这提供了一个可以在其中编写 thunk 的 createSlice 版本。可以使用该 createAppSlice 方法定义包含 fetchPosts thunk 的 postsSlice。当这样做时,其他一些事情会发生变化:
- 在使用 ts 时,不能直接传入 RootState 泛型,所以必须执行 getState() as RootState 来强制转换它
- 可以将处理 thunk 操作的所有 Reducer 作为选项的一部分传递给
create.asyncThunk(),并从 extraReducers 字段中删除它们:
const asyncPostsSlice = createAppSlice({
name: 'asyncPosts',
initialState: initialState,
reducers: (create) => {
return {
fetchPosts: create.asyncThunk(async () => {
const response = await client.get('/fakeApi/posts')
return response.data
}, {
options: {
condition: (arg, thunkApi) => {
const postsStatus = thunkApi.getState().asyncPosts.status
return postsStatus === 'idle'
}
},
pending: (state, action) => {
state.status = 'pending'
},
fulfilled: (state, action) => {
state.status = 'succeeded'
state.posts.push(...action.payload)
},
rejected: (state, action) => {
state.status = 'failed'
state.error = action.error.message ?? 'Unknown Error'
}
})
}
},
extraReducers: (builder) => {
builder.addCase(userLoggedOut, (state) => {
return initialState
})
}
})
export const { postAdded, postUpdated, reactionAdded, fetchPosts } = asyncPostsSlice.actions
请记住,create 回调语法是可选的!唯一需要使用它的时候是真的想在 createSlice 内部编写 thunk。也就是说,它确实消除了使用 PayloadAction 类型的需要,并且也减少了 extraReducers。
加载用户
看一下帖子,就会发现一个问题:作者未知。这是因为帖子条目是由虚假 API 服务器随机生成的,每次重新加载页面时,该服务器也会随机生成一组虚假用户。需要更新用户切片以在应用启动时获取这些用户。
新建 users/asyncUsersSlice.js 文件,创建另一个异步 thunk 以从 API 获取用户并返回它们,然后处理 extraReducers 切片字段中的 fulfilled 操作。暂时不用担心加载状态:
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import { selectCurrentUsername } from "../auth/authSlice";
import { client } from "../../api/client";
export const fetchUsers = createAsyncThunk('asyncUsers/fetchUsers', async () => {
const request = await client.get('/fakeApi/users')
return request.data
})
const initialState = []
const usersSlice = createSlice({
name: 'asyncUsers',
initialState,
reducers: {},
selectors: {
selectAllUsers: usersState => usersState,
selectUserById: (usersState, id) => usersState.find(user => user.id === id)
},
extraReducers: (builder) => {
builder.addCase(fetchUsers.fulfilled, (state, action) => {
return action.payload
})
}
})
export default usersSlice.reducer
export const {selectAllUsers, selectUserById} = usersSlice.selectors
export const selectCurrentUser = (state) => {
const currentUsername = selectCurrentUsername(state)
return selectUserById(state, currentUsername)
}
这次 case reducer 根本不使用 state 变量。而是直接返回 action.payload。Immer 使得可以通过两种方式更新状态:要么改变现有的状态值,要么返回新的结果。如果返回一个新值,它将用返回的值完全替换现有状态。(请注意,如果想手动返回新值,则需要编写可能需要的任何不可变更新逻辑。)
初始状态是一个空数组,可能已经完成了 state.push(...action.payload) 来改变它。但是,在我们的例子中,我们确实希望用服务器返回的任何内容替换用户列表,这可以避免意外复制状态中的用户列表的任何机会。
只需要获取用户列表一次,并且希望在应用启动时正确执行此操作。可以在 main.tsx 文件中执行此操作,并直接分派 fetchUsers thunk,因为有 store:
import { worker } from "./api/server";
import { fetchUsers } from "./features/users/asyncUsersSlice";
async function mockStart() {
await worker.start({ onUnhandledRequest: 'bypass' })
store.dispatch(fetchUsers())
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<Provider store={store}>
<React.Suspense>
<Router><App /></Router>
</React.Suspense>
</Provider>
</React.StrictMode>
);
}
mockStart()
注意,这是在启动时获取数据的有效方法。这实际上在我们开始渲染 React 组件之前启动了获取过程,因此数据应该更快地可用。(请注意,也可以通过使用 React Router 数据加载器 来实现。) 现在,每个帖子都应该显示用户名,并且还应该在
<AsyncAddPostForm>的 “作者” 下拉列表中显示相同的用户列表。
添加新帖子
当从 <AddPostForm> 添加新帖子时,该帖子只会添加到应用内的 Redux Store 中。实际上需要进行一个 API 调用,在假 API 服务器中创建新的帖子条目,因此它是 “saved”。(由于这是一个假 API,如果重新加载页面,新帖子将不会保留,但如果有一个真正的后端服务器,那么下次重新加载时它将可用。)
使用 Thunk 发送数据
可以使用 createAsyncThunk 来发送数据,而不仅仅是获取数据。创建一个 thunk,它接受 <AsyncAddPostForm> 中的值作为参数,并对假 API 进行 HTTP POST 调用以保存数据。
在此过程中,将更改在 reducer 中使用新 post 对象的方式。目前 asyncPostsSlice 正在 prepare 回调中为 postAdded 创建一个新的帖子对象,并为该帖子生成一个新的唯一 ID。在大多数将数据保存到服务器的应用中,服务器将负责生成唯一 ID 并填写任何额外字段,并且通常会在其响应中返回完整的数据。因此,可以向服务器发送一个像 { title, content, user: userId } 这样的请求体,然后获取它发回的完整的 post 对象并将其添加到 asyncPostsSlice 状态中。
export const fetchPosts = createAsyncThunk('asyncPosts/fetchPosts', async () => {
console.log('asyncPosts');
const response = await client.get('/fakeApi/posts')
console.log('data: ', response.data);
return response.data
}, {
condition: (arg, thunkApi) => {
const postsStatus = selectPostsStatus(thunkApi.getState())
return postsStatus === 'idle'
}
})
const asyncPostsSlice = createSlice({
name: 'asyncPosts',
initialState: initialState,
reducers: {
postUpdated: (state, action) => {
……
},
reactionAdded: (state, action) => {
……
}
},
selectors: {
……
},
extraReducers: (builder) => {
builder.addCase(addNewPost.fulfilled, (state, action) => {
state.posts.push(action.payload)
})
}
})
export const { postAdded, postUpdated, reactionAdded } = asyncPostsSlice.actions
检查组件中的 Thunk 结果
新建 <AsyncAddPostForm> 以调度 addNewPost thunk,而不是旧的 postAdded 操作。由于这是对服务器的另一个 API 调用,因此需要一些时间并且可能会失败。addNewPost thunk 会自动将其 pending/fulfilled/rejected 操作分派到 Redux Store。
如果愿意,我们可以使用第二个加载枚举来跟踪 postsSlice 中的请求状态。但是,对于此示例,让我们将加载状态跟踪限制在组件内,以显示还有什么可能。
如果在等待请求时可以禁用 “保存帖子” 按钮,那就太好了,这样用户就不会意外地尝试保存帖子两次。如果请求失败,还想在表单中显示错误消息,或者只是将其记录到控制台。可以让组件逻辑等待 async thunk 完成,并在完成后检查结果:
import { useDispatch, useSelector } from "react-redux";
import { selectAllUsers } from "../users/asyncUsersSlice";
import { addNewPost } from "./asyncPostsSlice";
import { useState } from "react";
export default function AddPostForm() {
const dispatch = useDispatch()
const users = useSelector(selectAllUsers)
const [addRequestStatus, setAddRequestStatus] = useState('idle')
const handleSubmit = async (e) => {
e.preventDefault()
const { elements } = e.target
const title = elements.postTitle.value
const content = elements.postContent.value
const userId = elements.postAuthor.value
try {
setAddRequestStatus('pending')
await dispatch(addNewPost({title, content, user: userId})).unwrap()
e.target.reset()
} catch (error) {
console.error('Failed to save the post: ', error)
} finally {
setAddRequestStatus('idle')
}
}
const usersOptions = users.map((user) => <option key={user.id} value={user.id}>{user.name}</option>)
return (<section>
<h2>Add a New Post</h2>
<form onSubmit={handleSubmit}>
<label htmlFor="postTitle">Post Title: </label>
<input id="postTitle" type="text" required></input>
<br></br>
<label htmlFor="postAuthor">Author: </label>
<select id="postAuthor" required>
<option value=''></option>
{usersOptions}
</select>
<br></br>
<label htmlFor="postContent">Post Content: </label>
<textarea id="postContent" name="postContent" required></textarea>
<br></br>
<button disabled={addRequestStatus !== 'idle'}>Save Post</button>
</form>
</section>)
}
可以添加一个加载状态字段作为 React useState 钩子,类似于在 postsSlice 中跟踪加载状态。在这里只想知道请求是否正在进行。
当调用 dispatch(addNewPost()) 时,异步 thunk 从 dispatch 返回一个 Promise。await 可以在这里确保知道 thunk 何时完成其请求。但是还不知道该请求是成功还是失败。
createAsyncThunk 在内部处理任何错误,因此在日志中看不到任何有关 “rejected Promise” 的消息。然后它返回它调度的最终操作:如果成功则执行 fulfilled 操作,如果失败则执行 rejected 操作。这意味着 await dispatch(someAsyncThunk()) 始终是 “succeeds”,结果是操作对象本身。
通常希望编写逻辑来查看所发出的实际请求的成功或失败。Redux Toolkit 向返回的 Promise 添加了一个 unwrap() 函数,它将返回一个新的 Promise,该 Promise 要么具有来自 fulfilled 操作的实际 action.payload 值,要么如果是 rejected 操作则会抛出错误。这就可以使用正常的 try/catch 逻辑来处理组件中的成功和失败。因此,如果帖子创建成功,将清除输入字段以重置表单,如果失败,将把错误记录到控制台。
如果想查看 addNewPost API 调用失败时会发生什么,请尝试创建一个新帖子,其中 “content” 字段仅包含单词 “error”(不带引号)。服务器将看到这一点并发回失败的响应,因此应该看到一条记录到控制台的消息。
总结
Redux 使用名为 “中间件” 的插件来启用异步逻
- 标准异步中间件称为 redux-thunk,它包含在 Redux Toolkit 中
- Thunk 函数接收 dispatch 和 getState 作为参数,并可以将它们用作异步逻辑的一部分
可以调度其他操作来帮助跟踪 API 调用的加载状态
- 典型的模式是在调用之前调度 “pending” 操作,然后调度包含数据的 “success” 或包含错误的 “failure” 操作
- 加载状态通常应存储为枚举,例如 ‘idle’ 、 ‘pending’ 、 ‘succeeded’ 、 ‘rejected’
Redux Toolkit 有一个 createAsyncThunk API 可以分派这些操作
- createAsyncThunk 接受应返回 Promise 的 “payload 创建者” 回调,并自动生成
pending/fulfilled/rejected操作类型 - 生成的动作创建器(如 fetchPosts)会根据返回的 Promise 调度这些动作
- 可以使用 extraReducers 字段监听 createSlice 中的这些操作类型,并根据这些操作更新 reducer 中的状态。
- createAsyncThunk 有一个 condition 选项,可用于根据 Redux 状态取消请求
- thunks 可以返回 promise。具体来说,对于 createAsyncThunk,可以
await dispatch(someThunk()).unwrap()在组件级别处理请求成功或失败。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)