赞
踩
上边的案例我们一直在使用Redux核心库来使用Redux,除了Redux核心库外Redux还为我们提供了一种使用Redux的方式——Redux Toolkit。它的名字起的非常直白,Redux工具包,简称RTK。RTK可以帮助我们处理使用Redux过程中的重复性工作,简化Redux中的各种操作。
Redux Toolkit 是官方推荐的编写 Redux 逻辑的方法。 它包含我们对于构建 Redux 应用程序必不可少的包和函数。 Redux Toolkit 的构建简化了大多数 Redux 任务,防止了常见错误,并使编写 Redux 应用程序变得更加容易。可以说 Redux Toolkit 就是目前 Redux 的最佳实践方式。
为了方便后面内容,之后 Redux Toolkit 简称 RTK
Redux 核心库是故意设计成非定制化的样子(unopinionated)。怎么做完全取决于你,例如配置 store,你的 state 存什么东西,以及如何构建 reducer。
有些时候这样挺好,因为有很高的灵活性,但我们又不总是需要这么高的自由度。有时,我们只是想以最简单的方式上手,并想要一些良好的默认行为能够开箱即用。或者,也许你正在编写一个更大的应用程序并发现自己正在编写一些类似的代码,而你想减少必须手工编写的代码量。
Redux Toolkit 它最初是为了帮助解决有关 Redux 的三个常见问题而创建的:
通过遵循我们推荐的最佳实践,提供良好的默认行为,捕获错误并让你编写更简单的代码,React Toolkit 使得编写好的 Redux 应用程序以及加快开发速度变得更加容易。 Redux Toolkit 对所有 Redux 用户都有帮助,无论技能水平或者经验如何。可以在新项目开始时添加它,也可以在现有项目中将其用作增量迁移的一部分。
学习的最佳方法我个人觉得还是看官方文档比较权威: 中文官方文档、英文官方文档。
安装,无论是RTK还是Redux,在React中使用时react-redux都是必不可少,所以使用RTK依然需要安装两个包:react-redux和@reduxjs/toolkit。
npm
npm install react-redux @reduxjs/toolkit -S
yarn
yarn add react-redux @reduxjs/toolkit
在官方文档中其实提供了完整的 RTK 项目创建命令,但咱们学习就从基础的搭建开始吧。
安装完相关包以后开始编写基本的 RTK 程序
创建 slice 需要一个字符串名称来标识切片、一个初始 state 以及一个或多个定义了该如何更新 state 的 reducer 函数。slice 创建后 ,我们可以导出 slice 中生成的 Redux action creators 和 reducer 函数。
store/features/counterSlice.js
import { createSlice } from '@reduxjs/toolkit' const initialState = { value: 0, } // 创建一个Slice export const counterSlice = createSlice({ name: 'counter', initialState, reducers: { // 定义一个加的方法 increment: state => { state.value += 1 }, // 定义一个减的方法 decrement: state => { state.value -= 1 }, }, }) console.log('counterSlice', counterSlice) console.log('counterSlice.actions', counterSlice.actions) // 导出加减方法 export const { increment, decrement } = counterSlice.actions // 暴露reducer export default counterSlice.reducer
createSlice是一个全自动的创建reducer切片的方法,在它的内部调用就是createAction和createReducer,之所以先介绍那两个也是这个原因。createSlice需要一个对象作为参数,对象中通过不同的属性来指定reducer的配置信息。
createSlice(configuration object)
配置对象中的属性:
总的来说,使用createSlice创建切片后,切片会自动根据配置对象生成action和reducer,action需要导出给调用处,调用处可以使用action作为dispatch的参数触发state的修改。reducer需要传递给configureStore以使其在仓库中生效。
我们可以看看counterSlice和counterSlice.actions是什么样子
下一步,我们需要从计数切片中引入 reducer 函数,并将它添加到我们的 store 中。通过在 reducer 参数中定义一个字段,我们告诉 store 使用这个 slice reducer 函数来处理对该状态的所有更新。
我们以前直接用redux是这样的
const reducer = combineReducers({
counter:counterReducers
});
const store = createStore(reducer);
store/index.js
切片的reducer属性是切片根据我们传递的方法自动创建生成的reducer,需要将其作为reducer传递进configureStore的配置对象中以使其生效:
import { configureStore } from '@reduxjs/toolkit'
import counterSlice from './features/counterSlice'
// configureStore创建一个redux数据
const store = configureStore({
// 合并多个Slice
reducer: {
counter: counterSlice,
},
})
export default store
main.js
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
// redux toolkit
import { Provider } from 'react-redux'
import store from './store'
ReactDOM.createRoot(document.getElementById('root')).render(
<Provider store={store}>
<App />
</Provider>,
)
现在我们可以使用 React-Redux 钩子让 React 组件与 Redux store 交互。我们可以使用 useSelector 从 store 中读取数据,使用 useDispatch dispatch actions。
App.jsx
import React from 'react' import { useDispatch, useSelector } from 'react-redux' // 引入对应的方法 import { increment, decrement } from './store/features/counterSlice' export default function App() { const count = useSelector(state => state.counter.value) const dispatch = useDispatch() return ( <div style={{ width: 100, margin: '100px auto' }}> <button onClick={() => dispatch(increment())}>+</button> <span>{count}</span> <button onClick={() => dispatch(decrement())}>-</button> </div> ) }
现在,每当你点击”递增“和“递减”按钮。
这是关于如何通过 React 设置和使用 Redux Toolkit 的简要概述。 回顾细节:
这个工具帮我们封装好了很多操作,虽然很方便,但是刚使用很多地方不是那么习惯。
每个文件的代码就不贴了,和上面一样的,可以复制到文本结合看
store/features/counterSlice.js
早些时候,我们看到单击视图中的不同按钮会 dispatch 三种不同类型的 Redux action:
我们知道 action 是带有 type 字段的普通对象,type 字段总是一个字符串,并且我们通常有 action creator 函数来创建和返回 action 对象。那么在哪里定义 action 对象、类型字符串和 action creator 呢?
我们_可以_每次都手写。但是,那会很乏味。此外,Redux 中_真正_重要的是 reducer 函数,以及其中计算新状态的逻辑。
Redux Toolkit 有一个名为 createSlice 的函数,它负责生成 action 类型字符串、action creator 函数和 action 对象的工作。你所要做的就是为这个 slice 定义一个名称,编写一个包含 reducer 函数的对象,它会自动生成相应的 action 代码。name 选项的字符串用作每个 action 类型的第一部分,每个 reducer 函数的键名用作第二部分。因此,“counter” 名称 + “increment” reducer 函数生成了一个 action 类型 {type: “counter/increment”}。(毕竟,如果计算机可以为我们做,为什么要手写!)
除了 name 字段,createSlice 还需要我们为 reducer 传入初始状态值,以便在第一次调用时就有一个 state。在这种情况下,我们提供了一个对象,它有一个从 0 开始的 value 字段。
我们可以看到这里有三个 reducer 函数,它们对应于通过单击不同按钮 dispatch 的三种不同的 action 类型。
createSlice 会自动生成与我们编写的 reducer 函数同名的 action creator。我们可以通过调用其中一个来检查它并查看它返回的内容:
console.log(counterSlice.actions.increment())
// {type: "counter/increment"}
它还生成知道如何响应所有这些 action 类型的 slice reducer 函数:
const newState = counterSlice.reducer(
{ value: 10 },
counterSlice.actions.increment()
)
console.log(newState)
// {value: 11}
Reducer 必需符合以下规则:
但为什么这些规则很重要?有几个不同的原因:
“不可变更新(Immutable Updates)” 这个规则尤其重要,值得进一步讨论。
前面讲过 “mutation”(更新已有对象/数组的值)与 “immutability”(认为值是不可以改变的)
在 Redux 中,永远 不允许在 reducer 中更改 state 的原始对象!
// ❌ 非法 - 默认情况下,这将更改 state!
state.value = 123
不能在 Redux 中更改 state 有几个原因:
所以如果我们不能更改原件,我们如何返回更新的状态呢?
Reducer 中必需要先创建原始值的副本,然后可以改变副本。
// ✅ 这样操作是安全的,因为创建了副本
return {
...state,
value: 123
}
我们已经看到我们可以手动编写 immutable 更新。但是,手动编写不可变的更新逻辑确实繁琐,而且在 reducer 中意外改变状态是 Redux 用户最常犯的一个错误。
这就是为什么 Redux Toolkit 的 createSlice 函数可以让你以更简单的方式编写不可变更新!
createSlice 内部使用了一个名为 Immer 的库。 Immer 使用一种称为 “Proxy” 的特殊 JS 工具来包装你提供的数据,当你尝试 ”mutate“ 这些数据的时候,奇迹发生了,Immer 会跟踪你尝试进行的所有更改,然后使用该更改列表返回一个安全的、不可变的更新值,就好像你手动编写了所有不可变的更新逻辑一样。
所以,下面的代码:
function handwrittenReducer(state, action) {
return {
...state,
first: {
...state.first,
second: {
...state.first.second,
[action.someId]: {
...state.first.second[action.someId],
fourth: action.someValue
}
}
}
}
}
可以变成这样:
function reducerWithImmer(state, action) {
state.first.second[action.someId].fourth = action.someValue
}
变得非常易读!
但,还有一些非常重要的规则要记住:
你只能在 Redux Toolkit 的 createSlice 和 createReducer 中编写 “mutation” 逻辑,因为它们在内部使用 Immer!如果你在没有 Immer 的 reducer 中编写 mutation 逻辑,它将改变状态并导致错误!
上面的项目中固定的加一减一,那如果我们想加多少就能动态加多少,那就需要传参。那如何传参呢?
接收参数的方式和 redux 一样,我们可以通过 action 来接收参数,如下:
store/features/counterSlice.js
import { createSlice } from '@reduxjs/toolkit' // 创建一个Slice export const counterSlice = createSlice({ // ... reducers: { incrementByAmount: (state, action) => { // action 里面有 type 和 payload 两个属性,所有的传参都在payload里面 console.log(action) state.value += action.payload }, }, }) // 导出加减方法 export const { increment, decrement, incrementByAmount } = counterSlice.actions // 暴露reducer export default counterSlice.reducer
incrementByAmount的action参数
和 redux 的传参一样,如下:
import React, { useState } from 'react' import { useDispatch, useSelector } from 'react-redux' // 引入对应的方法 import { incrementByAmount } from './store/features/counterSlice' export default function App() { const count = useSelector(state => state.counter.value) const dispatch = useDispatch() const [amount, setAmount] = useState(1) return ( <div style={{ width: 500, margin: '100px auto' }}> <input type="text" value={amount} onChange={e => setAmount(e.target.value)} /> <button onClick={() => dispatch(incrementByAmount(Number(amount) || 0))}> Add Amount </button> <span>{count}</span> </div> ) }
注意这里reducer的action中如果要传入参数,只能是一个payload,如果是多个参数的情况,那就需要封装成一个payload的对象。
以一个常见的todo案例来讲解
store/features/todoSlice.js
import { createSlice, nanoid } from '@reduxjs/toolkit' const initialState = { todoList: [], } // 创建一个Slice export const todoSlice = createSlice({ name: 'todo', initialState, reducers: { addTodo: (state, action) => {} }, }, }) // 导出加减方法 export const { addTodo } = todoSlice.actions // 暴露reducer export default todoSlice.reducer
store/index.js
import { configureStore } from '@reduxjs/toolkit'
import counterSlice from './features/counterSlice'
import todoSlice from './features/todoSlice'
// configureStore创建一个redux数据
const store = configureStore({
// 合并多个Slice
reducer: {
counter: counterSlice,
todo: todoSlice,
},
})
export default store
Todo.jsx
import React from 'react' import { useDispatch, useSelector } from 'react-redux' // 引入对应的方法 import { addTodo } from '../store/features/todoSlice' export default function Todo() { const todoList = useSelector(state => state.todo.todoList) const dispatch = useDispatch() return ( <div> <p>任务列表</p> <ul> {todoList.map(todo => ( <li key={todo.id}> <input type="checkbox" defaultChecked={todo.completed} /> {todo.content} </li> ))} </ul> <button onClick={() => dispatch(addTodo('敲代码'))}>增加一个todo</button> </div> ) }
我们刚刚看到 createSlice 中的 action creator 通常期望一个参数,它变成了 action.payload。这简化了最常见的使用模式,但有时我们需要做更多的工作来准备 action 对象的内容。 在我们的 postAdded 操作的情况下,我们需要为新todo生成一个唯一的 ID,我们还需要确保有效 payload 是一个看起来像 {id, content, completed} 的对象。
现在,我们正在 React 组件中生成 ID 并创建有效 payload 对象,并将有效 payload 对象传递给 addTodo。 但是,如果我们需要从不同的组件 dispatch 相同的 action,或者准备 payload 的逻辑很复杂怎么办? 每次我们想要 dispatch action 时,我们都必须复制该逻辑,并且我们强制组件确切地知道此 action 的有效 payload 应该是什么样子。
如果 action 需要包含唯一 ID 或其他一些随机值,请始终先生成该随机值并将其放入 action 对象中。 Reducer 中永远不应该计算随机值,因为这会使结果不可预测。
幸运的是,createSlice 允许我们在编写 reducer 时定义一个 prepare 函数。 prepare 函数可以接受多个参数,生成诸如唯一 ID 之类的随机值,并运行需要的任何其他同步逻辑来决定哪些值进入 action 对象。然后它应该返回一个包含 payload 字段的对象。(返回对象还可能包含一个 meta 字段,可用于向 action 添加额外的描述性值,以及一个 error 字段,该字段应该是一个布尔值,指示此 action 是否表示某种错误。)
rtk还提供了一个nanoid方法,用于生成一个固定长度的随机字符串,类似uuid功能。
可以打印dispatch(addTodo(’敲代码‘))的结果看到,返回了一个带有payload字段的action
import { createSlice, nanoid } from '@reduxjs/toolkit' // 创建一个Slice export const todoSlice = createSlice({ name: 'todo', initialState, reducers: { addTodo: { // 这个函数就是我们平时直接写在这的函数( addTodo: (state, action) => {}) reducer(state, aciton) { console.log('addTodo-reducer执行') const { id, content } = aciton.payload state.todoList.push({ id, content, completed: false }) }, // 预处理函数,返回值就是reducer函数接收的pyload值, 必须返回一个带有payload字段的对象 prepare(content) { console.log('prepare参数', content) return { payload: { id: nanoid(), content, }, } }, }, }, })
就其本身而言,Redux store 对异步逻辑一无所知。它只知道如何同步 dispatch action,通过调用 root reducer 函数更新状态,并通知 UI 某些事情发生了变化。任何异步都必须发生在 store 之外。
但是,如果你希望通过调度或检查当前 store 状态来使异步逻辑与 store 交互,该怎么办? 这就是 Redux middleware 的用武之地。它们扩展了 store,并允许你:
Redux 有多种异步 middleware,每一种都允许你使用不同的语法编写逻辑。最常见的异步 middleware 是 redux-thunk,它可以让你编写可能直接包含异步逻辑的普通函数。Redux Toolkit 的 configureStore 功能默认自动设置 thunk middleware,我们推荐使用 thunk 作为 Redux 开发异步逻辑的标准方式。
thunk最重要的思想,就是可以接受一个返回函数的action creator。如果这个action creator 返回的是一个函数,就执行它,如果不是,就按照原来的action执行。
正因为这个action creator可以返回一个函数,那么就可以在这个函数中执行一些异步的操作。
Thunks 通常还可以使用 action creator 再次 dispatch 普通的 action,比如 dispatch(increment())
为了与 dispatch 普通 action 对象保持一致,我们通常将它们写为 thunk action creators,它返回 thunk 函数。这些 action creator 可以接受可以在 thunk 中使用的参数。
const incrementAsync = amount => {
return (dispatch, getState) => {
setTimeout(() => {
dispatch(incrementByAmount(amount))
}, 1000)
}
}
incrementAsync函数就返回了一个函数,将dispatch作为函数的第一个参数传递进去,在函数内进行异步操作就可以了。
Thunk 通常写在 “slice” 文件中。createSlice 本身对定义 thunk 没有任何特殊支持,因此你应该将它们作为单独的函数编写在同一个 slice 文件中。这样,他们就可以访问该 slice 的普通 action creator,并且很容易找到 thunk 的位置。
增加一个延时器
store/features/counterSlice.js
import { createSlice } from '@reduxjs/toolkit' const initialState = { value: 0, } // 创建一个Slice export const counterSlice = createSlice({ name: 'counter', initialState, reducers: { incrementByAmount: (state, action) => { // action 里面有 type 和 payload 两个属性,所有的传参都在payload里面 state.value += action.payload }, }, }) const {incrementByAmount } = counterSlice.actions export const incrementAsync = amount => { return (dispatch, getState) => { const stateBefore = getState() console.log('Counter before:', stateBefore.counter) setTimeout(() => { dispatch(incrementByAmount(amount)) const stateAfter = getState() console.log('Counter after:', stateAfter.counter) }, 1000) } } // 暴露reducer export default counterSlice.reducer
`App.jsx
import React, { useState } from 'react' import { useDispatch, useSelector } from 'react-redux' // 引入对应的方法 import { incrementAsync } from './store/features/counterSlice' export default function App() { const count = useSelector(state => state.counter.value) const dispatch = useDispatch() const [amount, setAmount] = useState(1) return ( <div style={{ width: 500, margin: '100px auto' }}> <input type="text" value={amount} onChange={e => setAmount(e.target.value)} /> <button onClick={() => dispatch(incrementAsync(Number(amount) || 0))}> Add Async </button> <span>{count}</span> </div> ) }
Thunk 内部可能有异步逻辑,例如 setTimeout、Promise 和 async/await。这使它们成为使用 AJAX 发起 API 请求的好地方。
Redux 的数据请求逻辑通常遵循以下可预测的模式:
这些步骤不是 必需的,而是常用的。(如果你只关心一个成功的结果,你可以在请求完成时发送一个“成功” action ,并跳过“开始”和“失败” action 。)
Redux Toolkit 提供了一个 createAsyncThunk API 来实现这些 action 的创建和 dispatch,我们很快就会看看如何使用它。
如果我们手动编写一个典型的 async thunk 的代码,它可能看起来像这样:
const getRepoDetailsStarted = () => ({ type: 'repoDetails/fetchStarted' }) const getRepoDetailsSuccess = repoDetails => ({ type: 'repoDetails/fetchSucceeded', payload: repoDetails }) const getRepoDetailsFailed = error => ({ type: 'repoDetails/fetchFailed', error }) const fetchIssuesCount = (org, repo) => async dispatch => { dispatch(getRepoDetailsStarted()) try { const repoDetails = await getRepoDetails(org, repo) dispatch(getRepoDetailsSuccess(repoDetails)) } catch (err) { dispatch(getRepoDetailsFailed(err.toString())) } }
但是,使用这种方法编写代码很乏味。每个单独的请求类型都需要重复类似的实现:
createAsyncThunk 实现了这套模式:通过生成 action type 和 action creator 并生成一个自动 dispatch 这些 action 的 thunk。你提供一个回调函数来进行异步调用,并把结果数据返回成 Promise。
Redux Toolkit 的 createAsyncThunk API 生成 thunk,为你自动 dispatch 那些 “start/success/failure” action。
让我们从添加一个 thunk 开始,该 thunk 将进行 AJAX 调用。
store/features/counterSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit' // 请求电影列表 const reqMovieListApi = () => fetch( 'https://pcw-api.iqiyi.com/search/recommend/list?channel_id=1&data_type=1&mode=24&page_id=1&ret_num=48', ).then(res => res.json()) const initialState = { status: 'idel', list: [], totals: 0, } // thunk函数允许执行异步逻辑, 通常用于发出异步请求。 // createAsyncThunk 创建一个异步action,方法触发的时候会有三种状态: // pending(进行中)、fulfilled(成功)、rejected(失败) export const getMovieData = createAsyncThunk('movie/getMovie', async () => { const res = await reqMovieListApi() return res.data })
createAsyncThunk 接收 2 个参数:
Payload creator 通常会进行某种 AJAX 调用,并且可以直接从 AJAX 调用返回 Promise,或者从 API 响应中提取一些数据并返回。我们通常使用 JS async/await 语法来编写它,这让我们可以编写使用 Promise 的函数,同时使用标准的 try/catch 逻辑而不是 somePromise.then() 链式调用。
在这种情况下,我们传入 ‘movie/getMovie’ 作为 action 类型的前缀。我们的 payload 创建回调等待 API 调用返回响应。响应对象的格式为 {data: []},我们希望我们 dispatch 的 Redux action 有一个 payload,也就是电影列表的数组。所以,我们提取 response.data,并从回调中返回它。
当调用 dispatch(getMovieData()) 的时候,getMovieData 这个 thunk 会首先 dispatch 一个 action 类型为’movie/getMovie/pending’:
我们可以在我们的 reducer 中监听这个 action 并将请求状态标记为 “loading 正在加载”。
一旦 Promise 成功,getMovieData thunk 会接受我们从回调中返回的 response.data ,并 dispatch 一个 action,action 的 payload 为 接口返回的数据(response.data ),action 的 类型为 ‘movie/getMovie/fulfilled’。
有时 slice 的 reducer 需要响应 没有 定义到该 slice 的 reducers 字段中的 action。这个时候就需要使用 slice 中的 extraReducers 字段。
extraReducers 选项是一个接收名为 builder 的参数的函数。builder 对象提供了一些方法,让我们可以定义额外的 case reducer,这些 reducer 将响应在 slice 之外定义的 action。我们将使用 builder.addCase(actionCreator, reducer) 来处理异步 thunk dispatch 的每个 action。
在这个例子中,我们需要监听我们 getMovieData thunk dispatch 的 “pending” 和 “fulfilled” action 类型。这些 action creator 附加到我们实际的 getMovieData 函数中,我们可以将它们传递给 extraReducers 来监听这些 action:
const initialState = {
status: 'idel',
list: [],
totals: 0,
}
export const getMovieData = createAsyncThunk('movie/getMovie', async () => {
const res = await reqMovieListApi()
return res.data
})
// 创建一个 Slice
export const movieSlice = createSlice({
name: 'movie',
initialState,
// extraReducers 字段让 slice 处理在别处定义的 actions,
// 包括由 createAsyncThunk 或其他slice生成的actions。
extraReducers(builder) {
builder
.addCase(getMovieData.pending, state => {
console.log('声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/羊村懒王/article/detail/510844
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。