什么是Redux?
Redux 是一个使用叫做 “action” 的事件来管理和更新应用状态的模式和工具库 它以集中式 Store(centralized store)的方式对整个应用中使用的状态进行集中管理,其规则确保状态只能以可预测的方式更新。
在以下情况下使用 Redux:
- 应用中有很多 state 在多个组件中需要使用
- 应用 state 会随着时间的推移而频繁更新
- 更新 state 的逻辑很复杂
- 中型和大型代码量的应用,很多人协同开发
并非所有应用程序都需要 Redux。 花一些时间思考你正在构建的应用程序类型,并决定哪些工具最能帮助解决你正在处理的问题。
react + redux
核心概念
-
state
全局共享的状态
// ToDo 的初始State
const initState: IToDo[] = [
{
id: '1',
todo: "睡觉",
isDone: false
},
{
id: '2',
todo: "吃饭",
isDone: false
},
{
id: '3',
todo: "打豆豆",
isDone: false
},
];
-
reducer
修改 state 的纯函数
接收 初始state 和 action 为参数
export const todoReducer = (state = initState, action: IAction) => {
const { type, payload } = action;
switch (type) {
case "ADD_TODO":
return [...state, payload];
case "DEL_TODO":
return state.filter(item => item.id !== payload);
default:
return state;
}
}
-
action
修改 state 的动作
为对象时(同步),包含两个属性:type 表示做什么事;payload 额外的数据参数
为函数时(异步)
// 添加todo
export const addTodo = (payload: IToDo) => ({ type: 'ADD_TODO', payload })
// 删除todo
export const deleteTodo = (payload: string) => ({ type: 'DEL_TODO', payload })
-
store
redux 的核心模块,用于联接 action 和 reducer
方法:
- getState 获取 state
- dispatch 接收 action ,触发 reducer
- subscribe 当 state 状态改变时,重新执行回调,重新渲染
// We recommend using the configureStore method of the @reduxjs/toolkit package, which replaces createStore.
import { createStore, combineReducers } from "redux"
import { todoReducer } from "./reducers/todo"
import { countReducer } from "./reducers/count"
/*
combineReducers 内部,根据 key, 为每个 reducer 传入各自的 state,起到环境隔离作用
action.type 没有隔离, 如果 2 个 reducer 都有同名的 action.type, 则会同时触发。
当 dispatch 时,会触发所有的 reducer, 但是每个 reducer 只会处理自己的 state
*/
// 合并多个 reducer
const rootReducer = combineReducers({
todos: todoReducer,
count: countReducer
});
const store = createStore(rootReducer);
export default store;
在组件中使用redux
// src/index.tsx
import { createRoot } from 'react-dom/client';
import App from './App';
import store from './store';
const container = document.getElementById('root')!;
const root = createRoot(container);
root.render(<App />)
// 订阅 当 store 中的全局状态发生改变时,重新执行回调函数
store.subscribe(() => { root.render(<App />) })
// ToDo.tsx
const ToDo = () => {
const todos = store.getState().todos
const addTodos = () => {
const todo: IToDo = {
id: crypto.randomUUID(),
todo: 'test',
isDone: false
}
store.dispatch(addTodo(todo))
}
const deleteTodos = (id: string) => {
store.dispatch(deleteTodo(id))
}
return (<>
<>
<div className="container">
{
todos.map(item => (<div className="item" key={item.id}>
<span>{item.todo}</span>
<span onClick={() => { deleteTodos(item.id) }}>x</span>
</div>))
}
</div>
<button onClick={addTodos}>添加事项</button>
<button onClick={() => { store.dispatch(todoSameName()) }}>触发相同的Action</button>
</>
</>)
}
// Count.tsx
const Counter = () => {
const count = store.getState().count;
const todos = store.getState().todos
return (<>
<div>{count}</div>
<div>todos的数量:{todos.length}</div>
<button onClick={() => { store.dispatch(addCount(3)) }}>+3</button>
<button onClick={() => { store.dispatch(subCount(2)) }}>-2</button>
<button onClick={() => { store.dispatch(mulCount(4)) }}>*4</button>
<button onClick={() => { store.dispatch(divCount(2)) }}>/2</button>
<button onClick={() => { store.dispatch(countSameName()) }}>触发相同的Action</button>
</>)
}
redux-thunk 中间件处理异步
redux 的 dispatch 只能接收一个对象,但是有时候需要在 action creator 时进行异步操作,如果直接返回一个对象,Redux会无法处理异步操作,因为Redux只能处理纯对象的action。
redux-thunk中间件的作用是拦截action,如果action是一个函数而不是一个纯对象,redux-thunk会将dispatch和getState作为参数传递给该函数,并执行该函数。函数可以进行异步操作,并在适当的时候再次dispatch一个action,这个action可以是一个纯对象。
// store/index.ts
import { applyMiddleware } from "redux"
import thunk from "redux-thunk"
// ...
const store = createStore(rootReducer,applyMiddleware(thunk));
export default store
// actions/count.ts
// 异步加 3
import { Dispatch } from "redux";
export const addCountAsync = (payload: number) => {
return (dispatch: Dispatch<IAction>) => {
setTimeout(() => {
dispatch(addCount(payload))
}, 1000)
}
}
// Count.tsx
store.dispatch(asyncAddCount(3));
react + react-redux
React-Redux 使用流程图
核心 API
-
mapStateToProps
- 它接收整个 store state 作为第一个参数(必传)的函数
- 每次 store state 变更时都会调用它
- 返回一个包含组件所需数据的普通对象,对象中的每个字段都将成为实际组件中的 prop,字段中的值将用于确定你的组件是否需要重新渲染
const mapStateToProps = (state: IState, ownProps?) => { return { count: state.count, todos: state.todos }; };默认情况下,React Redux 针对
mapStateToProps返回的对象中的每个字段,使用===进行比较(“浅对比”检查)以判断其内容是否不同。如果任何字段发生更改,那么你的组件将被重新渲染,以便它可以接收更新的值作为 props。 -
mapDispatchToProps
-
函数形式
- 一个接收 dispatch并返回一个普通对象的函数
- 在内部调用 dispatch(),并接收 action 对象或传入 action creator
- 返回的对象中的字段会作为 UI 组件的 props
const mapDispatchToProps = (dispatch: Dispatch<IAction>, ownProps?) => { return { addCount: (count: number) => dispatch(addCount(count)), addCountAsync: (count: number) => dispatch(addCountAsync(count)), subCount: (count: number) => dispatch(subCount(count)), mulCount: (count: number) => dispatch(mulCount(count)), divCount: (count: number) => dispatch(divCount(count)) }; };-
使用 bindActionCreators 定义 mapDispatchToProps 函数
- bindActionCreators 接收 function (an action creator) 或者 object (每个字段都是一个 action creator)和 dispatch 两个参数
- 返回值和上面函数形式返回的对象相同
const mapDispatchToProps = (dispatch: Dispatch<IAction>) => { return bindActionCreators({ addCount, addCountAsync, subCount, mulCount, divCount }, dispatch) } -
对象形式
- 把mapDispatchToProps定义为一个充满 action creators 的对象而不是一个函数,
connect将在内部自动为你调用bindActionCreators
const mapDispatchToProps = { addCount, addCountAsync, subCount, mulCount, divCount }; - 把mapDispatchToProps定义为一个充满 action creators 的对象而不是一个函数,
-
-
connect
-
connect接收四种不同的、皆可选的参数。按照惯例,它们被称为:- mapStateToProps?: Function
- mapDispatchToProps?: Function
- mergeProps?:Function
- options?:Object
-
返回值是一个 wrapper(封套)函数,它接收你的组件并返回一个 wrapper 组件,其中包含注入到组件的 props。
-
在类组件中的使用
// CounterUI 组件
class CounterUI extends React.Component<any, ICountProps> {
render() {
const { count, todos, addCount, addCountAsync, subCount, mulCount, divCount } = this.props
return (<> ... </>)
}
}
// Counter容器组件
const Counter = connect(mapStateToProps, mapDispatchToProps)(CounterUI)
在 react hooks 中使用
const Counter = () => {
// 返回一个 Redux store 引用,该 store 与传递给 <Provider> 组件的 store 相同。
const store = useStore();
// 调用useDispatch函数会返回一个dispatch函数
const dispatch = useDispatch();
// useSelector声明了一个泛型,第一个是state的类型,第二表示返回值的类型
const count = useSelector<IState, IState['count']>((state) => state.count);
// 仅仅是示例!不要在实际的应用中这么做。
// 当 store state 变更时,组件不会自动更新
return <div>{store.getState()}</div>
}
react + redux toolkit
Redux Toolkit 简化了编写 Redux 逻辑和设置 store 的过程,目前 Redux 的最佳实践方式。
Redux-Toolkit 核心API
-
configureStore
- 封装 createStore 生成 store 实例
- 封装 combineReducers 合并 reducers
- 可以添加任何 Redux 中间件,默认情况下包含 redux-thunk
- 返回值:
getState,dispatch,subscribe三个核心方法
-
createAction
- 生成 action 创建函数
- 接收字符串参数作为 action type
- 返回 action 对象,默认包含 type 和 payload 属性
import { createAction } from "@reduxjs/toolkit" const modifyName = createAction('MODIFY_NAME'); console.log(modifyName());
上面的方式创建 action 函数并不能传递额外参数。可以传递额外参数的写法
import { createAction } from "@reduxjs/toolkit" const modifyName = createAction('MODIFY_NAME', (data) => { return { payload: data, meta: '', // optional error: '' // optional } }); console.log(modifyName('王五')); -
createReducer
-
接收两个参数:第一个参数初始值 initState,第二个参数回调函数或对象(RTK 2.0将移除对象形式,已过时)
-
使用Immer库,可以直接修改对象的属性值,而无需再返回一个新的对象。
-
immer是一个简化不可变数据更新的JavaScript库。它允许您以可变的方式编写代码,同时在背后自动处理不可变性。通过使用immer,您可以编写更简洁和易于理解的代码,同时避免手动处理不可变数据的复杂性。const initState = { name:"李四",age:18 } // 使用 immer 之前 return { ...state, name:action.payload } // 使用 immer state.name = action.payload -
第二个参数的传递
// 回调函数中接收一个构建器builder,builder构建器中有三个属性addCase,addMatcher,addDefaultCase // addCase 创建 case ,类似于自定义 reducer 函数中的 switch case // addMatcher 根据自定义匹配函数来处理逻辑,接收两个回调函数作为参数:第一个回调函数为匹配函数;第二个回调函数为匹配成功的处理函数 // addDefaultCase 没有匹配到任何已定义的 action 类型时进行的逻辑处理 import { createAction, createReducer } from "@reduxjs/toolkit" export const modifyName = createAction("MODIFY_NAME", (data) => { return { payload: data } }); export const modifyAge = createAction("MODIFY_AGE", (data) => ({ payload: data })); const initialState = { name: "张三", age: 18 }; export const userReducer = createReducer(initialState, (builder) => { builder.addCase(modifyName, (state, action: IAction) => { state.name = action.payload; console.log('addCase--modifyName'); }); builder.addCase(modifyAge, (state, action: IAction) => { state.age = action.payload; }); builder.addMatcher((action: IAction) => { return action.payload === '李四'; }, (state, action: IAction) => { state.name = action.payload; console.log('addMatcher--modifyName'); }) builder.addDefaultCase((state, action: IAction) => { // 什么也不做 // console.log(state, action); }); }); -
-
createSlice
- 接收一个包含三个主要选项字段的对象
name:一个字符串,将用作生成的 action types 的前缀initialState:reducer 的初始 statereducers:一个对象,其中键是字符串,值是处理特定 actions 的 “case reducer” 函数- 返回值:
import { createSlice } from "@reduxjs/toolkit" const initState: IToDo[] = [ { id: '1', todo: "睡觉", isDone: false }, { id: '2', todo: "吃饭", isDone: false }, { id: '3', todo: "打豆豆", isDone: false }, ]; export const todosSlice = createSlice({ name: 'todos', initialState: initState, reducers: { // 添加todo addTodo(state: IToDo[], action) { // console.log(state, action); /** * action * * payload: {id: '3f11e030-c15b-4910-8233-a91be220f55b', todo: 'test', isDone: false} * type: "todos/addTodo" */ state.push(action.payload); }, // 删除todo deleteTodo(state: IToDo[], action) { state.forEach((item, index) => { if (item.id === action.payload) { state.splice(index, 1); } }); }, } }) export const { addTodo, deleteTodo } = todosSlice.actions; console.log(todosSlice);-
createSelector
-
Reselect 提供了一个名为 createSelector 的函数来生成记忆化 Selector 。
createSelector接收一个或多个 input selector 函数,外加一个 output selector 作为参数,并返回一个新的 Selector 函数作为结果。 -
所有 input selector 的结果作为单独的参数提供给 output selector。
-
当你调用 Selector 时,Reselect 将使用你提供的所有参数运行你的 input selector,并查看返回的值。如果任何结果与之前的
===不同,它将重新运行 output selector,并将这些结果作为参数传递。如果所有结果都与上次相同,它将跳过重新运行 output selector,并返回之前缓存的最终结果。 -
计算属性
const count = (state: IInitialState, payload?: any) => { if (payload) { return state.value - payload } else { return state.value } } const other = (state: IInitialState, payload?: any) => { return state.other } // 计算属性 export const selectCount = createSelector( [count, other], (value, other) => { return value + other } ); /** * Reselect 内部会做的事情: * const firstArg = count(state, 2); * const secondArg = other(state); * * const result = outputSelector(firstArg, secondArg); * return result */ -
在组件中使用
import { selectCount } from './store/features/countSlice' // 可以将多个参数传递给 Selector,Reselect 将使用这些参数调用所有 input selector const count2 = useSelector((state: IState) => selectCount(state.count, 2)); const count3 = useSelector((state: IState) => selectCount(state.count));
-