Redux Toolkit是Redux官方推出的工具集,因为Redux编写过程中会写很多的样板代码,对开发者不太友好,因此官方推出Redux Toolkit,Redux Toolkit就是对Redux的二次封装,用于高效 Redux 开发,使 Redux 的使用变得简单,下面用 Redux Toolkit 实现 TodoList 案例
npm i @reduxjs/toolkit
目录结构
创建状态切片
对于状态切片,我们可以理解为就是 Reducer 和 Action 的集合体;在 Redux 中,原本 Reducer 函数和 Action 对象需要分别创建,现在通过状态切片替代,它会返回 Reducer 函数和 Action 对象
通过引入 createSlice 创建切片,给 createSlice 调用返回的 reducer 起一个别名 TodosReducer ,避免跟其他的 reducer 冲突
import { createSlice } from "@reduxjs/toolkit"
const { reducer: TodosReducer, actions } = createSlice()
配置状态切片
createSlice 定义 name 使用常量,后续 reducer 其他地方夜也会使用到常量,如需更改,只需更改常量的值,导出常量;initialState 初始状态值;reducers 就是一个个小的reducer集合,导出 reducer 跟 action
详细配置参考链接: https://redux-toolkit.js.org/api/createSlice.
export const TODOS_FEATURE_KEY = "todos"
const { reducer: TodosReducer, actions } = createSlice({
name: TODOS_FEATURE_KEY,
initialState: [],
reducers: {
addTodo: (state, action) => {
state.push(action.payload)
}
}
})
export const { addTodo } = actions
export default TodosReducer
创建 store
在 index.js 文件中创建 store, 使用 configureStore 配置 store,配置项中的reducer 对象的作用就是合并 reducer ,对应 redux 中的 combineReducers;devTools 配置为布尔值,这里非生产环境使用,
import { configureStore} from "@reduxjs/toolkit"
import TodosReducer, { TODOS_FEATURE_KEY } from "./todos.slice"
export default configureStore({
reducer: { // combineReducers
[TODOS_FEATURE_KEY]: TodosReducer
},
devTools: process.env.NODE_ENV !== "production"
})
Action 预处理
Action 被触发后, 可以通过 prepare 方法对 Action 进行预处理,就是在触发 reducer 之前去处理 Action ,Reducer. prepare 方法必须返回对象,原来的 createSlice 的配置 reducers 对象中的 reducer 函数也要配置为对象
addTodo: {
reducer: (state, action) => {
console.log(action)
state.push(action.payload)
},
prepare: todo => {
return {
payload: { cid: Math.random(), ...todo }
}
}
}
配置中间件
在 configureStore 配置 middleware ,首先需要配置默认中间件,再配置自己的中间件
store 中的完整代码
import { configureStore, getDefaultMiddleware } from "@reduxjs/toolkit"
import TodosReducer, { TODOS_FEATURE_KEY } from "./todos.slice"
import logger from "redux-logger"
export default configureStore({
reducer: {
[TODOS_FEATURE_KEY]: TodosReducer
},
devTools: process.env.NODE_ENV !== "production",
middleware: [...getDefaultMiddleware(), logger]
})
Redux Toolkit异步操作
Redux Toolkit异步操作需要用到Redux Toolkit工具集提供的方法createAsyncThunk,方法类似于 redux 中间件方法,用来创建执行异步操作的 actioncreator 函数;这个方法的第一个参数是 action 对象的 type 属性,是一个字符串;第二个参数是一个函数,函数作用执行异步操作,函数有两个参数,第一个参数是 payload ,就是在组件调用通过 createAsyncThunk 方法创建出来的 actioncreator 函数时传入的参数,第二个参数是 thunkAPI ,可以通过 thunkAPI 对象拿到 dispatch 方法,这样就可以通过 dispatch 触发另一个 action 保存得到的数据
export const loadTodos = createAsyncThunk("todos/loadTodos", payload =>
axios.get(payload).then(response => thunkAPI.dispatch(setTodos(response.data)))
)
在 createSlice 配置项 reducers 中创建 setTodos 函数,在 actions 中再次解构导出 setTodos
reducers: {
addTodo: {
reducer: todosAdapter.addOne,
prepare: todo => {
return {
payload: { cid: Math.random(), ...todo }
}
}
},
setTodos: (state, action) => {
action.payload.forEach(todo => state.push(todo))
}
}
export const { addTodo, setTodos } = actions
改进Redux Toolkit异步操作
在 createAsyncThunk 的时候,通过 thunkAPI 提供的 dispatch 触发另外一个action 保存状态,这样太麻烦了,在创建切片的时候(createSlice),可以传入配置 extraReducers ,extraReducers 也是配置 reducer 的,reducers 配置的是同步的 reducer ,extraReducers 配置的是异步的,loadTodos 返回一个Promise对象, extraReducers 中还可以监听接口请求的状态
export const loadTodos = createAsyncThunk("todos/loadTodos", payload =>
axios.get(payload).then(response => response.data)
)
extraReducers: {
[loadTodos.pending]: (state, action) => {
console.log("pending")
return state
},
[loadTodos.fulfilled]: (state, action) => {
action.payload.forEach(todo => state.push(todo))
}
}
实体适配器
实体适配器可以理解为一个容器,放入状态的容器,将状态放入实体适配器,实体适配器提供操作状态的各种方法,简化操作;通过 createEntityAdapter 方法调用得到容器对象 todosAdapter ;todosAdapter 中的 getInitialState 有两个属性,entities 和 ids,entities 存储的是一条一条的状态,ids 是对应的状态的 id,getInitialState 可以做为初始状态值;todosAdapter 还有 addOne 和 addMany 方法,方法就是往对象中添加一条和多条数据,addMany 会默认循环 action 中的数据
const todosAdapter = createEntityAdapter()
const { reducer: TodosReducer, actions } = createSlice({
name: TODOS_FEATURE_KEY,
initialState: todosAdapter.getInitialState(),
reducers: {
addTodo: {
reducer: (state, action) => {
console.log(action)
//state.push(action.payload)
todosAdapter.addOne(state, action.payload)
},
prepare: todo => {
return {
payload: { cid: Math.random(), ...todo }
}
}
},
setTodos: (state, action) => {
//action.payload.forEach(todo => state.push(todo))
todosAdapter.addMany(state, action.payload)
}
},
extraReducers: {
[loadTodos.pending]: (state, action) => {
console.log("pending")
return state
},
[loadTodos.fulfilled]: (state, action) => {
//action.payload.forEach(todo => state.push(todo))
todosAdapter.addMany(state, action.payload)
}
}
})
addOne 和 addMany 方法会检测第二个参数是不是 action ,如果是 action 会默认使用 payload 属性,因此 reducers 和 extraReducers 配置代码简化
reducers: {
addTodo: {
reducer: todosAdapter.addOne,
prepare: todo => {
return {
payload: { cid: Math.random(), ...todo }
}
}
},
setTodos: todosAdapter.addMany
},
extraReducers: {
[loadTodos.pending]: (state, action) => {
console.log("pending")
return state
},
[loadTodos.fulfilled]: todosAdapter.addMany
}
实体适配器要求每一个实体必须拥有 id 属性作为唯一标识,如果实体中的唯一标识字段不叫做 id,需要使用selectId 进行声明
配置链接: https://redux-toolkit.js.org/api/createEntityAdapter.
const todosAdapter = createEntityAdapter({
selectId: todo => todo.cid
})
状态选择器
提供从实体适配器中获取状态的快捷途径。需要用到 createSelector,使用链接: https://redux-toolkit.js.org/api/createSelector
import {
createSlice,
createAsyncThunk,
createEntityAdapter,
createSelector
} from "@reduxjs/toolkit"
const { selectAll } = todosAdapter.getSelectors()
export const selectTodos = createSelector(
state => state[TODOS_FEATURE_KEY],
selectAll
)
组件使用
import { useDispatch, useSelector } from "react-redux"
import { addTodo, loadTodos, selectTodos } from "../../Store/todos.slice"
import { useEffect } from "react"
function Main() {
const dispatch = useDispatch()
const todos = useSelector(selectTodos)
useEffect(() => {
dispatch(loadTodos("http://localhost:3001/todos"))
}, [])
return (
<section className="main">
<button onClick={() => dispatch(addTodo({ title: "测试任务" }))}>
添加任务
</button>
<ul className="todo-list">
{todos.map(todo => (
<li key={todo.cid}>
<div className="view">
<input className="toggle" type="checkbox" />
<label>{todo.title}</label>
<button className="destroy" />
</div>
<input className="edit" />
</li>
))}
</ul>
</section>
)
}
export default Main