redux 概念与使用
Redux 背后的基本思想
一个集中的位置来包含应用程序中的全局状态,并且基于状态编写代码时,需要遵循一些特定的模式来更新状态,以使代码具备可预测性。
Redux 三大原则
- 单一数据源:整个应用的 全局 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。
- state 是只读的:唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。
- 使用纯函数执行修改:为了描述 action 如何改变 state tree,你需要编写纯的 reducers。
Redux 希望所有状态更新都是不可变的
为了实现不可变更新,代码需要先复制已有的数据结构,并修改这些副本,从而达到不改变原有数据结构的目的。
Actions
一个 action 是一个普通的 JavaScript 对象,它具有一个 type 字段。可以将一个 action 看作是描述应用程序中发生的事件的一种方式。
const addTodoAction = {
type: "todos/todoAdded",
payload: "Buy milk",
};Action Creators
一个 action creator 是一个创建并返回 action 对象的函数。 通常,使用这些函数来避免每次手动编写 action 对象。
const addTodo = (text) => {
return {
type: "todos/todoAdded",
payload: text,
};
};Reducers
一个 reducer 是一个函数,它接收当前的状态和一个 action 对象,并决定是否需要更新状态,并返回新的状态:(state, action) => newState。 可以将 reducer 看作是一个事件监听器,根据接收到的 action(事件)类型处理事件。
Reducers 必须始终遵循一些特定规则
- reducer 函数只能基于 state 和 action 参数来计算新的 state 值。
- reducer 函数不允许直接修改当前的 state。相反,它们必须通过复制现有 state 并对副本进行更改的方式进行不可变更新。
- reducer 函数不得执行异步逻辑、计算随机值或引起其他 "副作用"。
reducer 内部逻辑步骤
- 检查 Reducer 是否关心此 action
- 如果是这样,则复制 state 并更新复制的值,然后将其返回。
- 否则,返回现有的 state(不作任何更改)。
const initialState = { value: 0 };
function counterReducer(state = initialState, action) {
// 检查reducer是否关心这个action
if (action.type === "counter/increment") {
// 如果是这样,创建state的副本
return {
...state,
// 使用新值更新副本
value: state.value + 1,
};
}
// 否则,返回现有state不变
return state;
}Reducers 中可以使用任何类型的逻辑来决定新的 state 应该是什么
为什么叫 Reducers:Redux 的 reducer 函数与数组的 reduce 函数的思想完全相同。它接受“前一个结果”(state)和“当前项”(action 对象),根据这些参数决定一个新的 state 值,并返回那个新 state。
reducer 将一组 action(随着时间的推移)简化为单个状态。
Store
Redux 应用程序的当前状态位于一个名为"store"的对象中。
创建 store 时需要传入一个 reducer 函数,store 具有一个名为"getState"的方法,该方法返回当前的 state 值:
import { configureStore } from "@reduxjs/toolkit";
const store = configureStore({ reducer: counterReducer });
console.log(store.getState());
// {value: 0}dispatch
Redux store 有一个叫做 dispatch 的方法。
更新 state 的唯一方式是调用 store.dispatch() 并传入一个 action 对象。
store 将运行其 reducer 函数并保存新的 state 值,可以调用 getState()方法检索更新后的值:
store.dispatch({ type: "counter/increment" });
console.log(store.getState());
// {value: 1}可以将派发(dispatch)action 想象为在应用程序中"触发事件"。
发生了某些事情,希望 store 知道它。 reducers 就像事件监听器,当它们听到自己感兴趣的 action 时,它们会响应地更新 state。
通常调用 action creators 来派发正确的 action:
const increment = () => {
return {
type: "counter/increment",
};
};
store.dispatch(increment());
console.log(store.getState());
// {value: 2}Selectors
Selectors 是一种函数,它们知道如何从 store 的 state 值中提取具体的信息。 随着应用程序的不断增长,这有助于避免重复逻辑,因为应用程序的不同部分需要读取相同的数据。
const selectCounterValue = (state) => state.value;
const currentValue = selectCounterValue(store.getState());
console.log(currentValue);
// 2Redux 应用程序数据流
单项数据流:
- 状态描述了应用程序在特定时间点的状态
- UI 根据该状态进行渲染
- 当某些事情发生时(例如用户点击按钮),根据发生的情况更新状态
- UI 会根据新状态进行重新渲染
对于 Redux,我们可以进一步详细地描述这些步骤:
初始化设置:
- 使用根 reducer 函数创建 Redux store
- Store 调用根 reducer 函数一次,并将其返回值保存为初始 state
- 首次渲染 UI 时,UI 组件访问 Redux store 的当前 state,并使用该数据决定渲染什么。他们还订阅了任何未来的 store 更新,以便知道 state 是否已更改。
更新:
- 应用程序发生某些事情,例如用户点击按钮
- 应用程序代码向 Redux store 分发一个 action,如 dispatch({type:'counter/increment'})
- Store 再次运行 reducer 函数,使用先前 state 和当前 action,并将其返回值保存为新 state
- Store 通知所有已订阅的 UI 的所有部分 store 已更新
- 每个需要从 store 获取数据的 UI 组件都会检查它们需要的状态部分是否已更改。
- 每个看到数据已更改的组件都会使用新数据强制重新渲染,以便更新在屏幕上显示的内容。
流程图:

Redux Toolkit 应用程序结构
创建 Redux store
// app/store.js
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "../features/counter/counterSlice";
export default configureStore({
reducer: {
counter: counterReducer,
},
});这里 Redux store 使用 Redux Toolkit 的 configureStore 函数创建的,configureStore 我们传递一个 reducer 参数。
应用程序可能由许多不同的功能组成,而每个功能可能都有自己的 reducer 函数。 当调用 configureStore 时,可以在一个对象中传入所有不同的 reducers。 对象中的键名将定义最终状态值的键名。
当传入类似{counter: counterReducer}的对象时,这意味着我们想要一个 state.counter Redux 状态对象部分, 并且希望在分派 action 时 counterReducer 函数负责决定是否以及如何更新 state.counter 部分。
Redux 允许使用不同类型的插件(“中间件”和“增强器”)自定义 store 设置。 configureStore 默认自动向 store 设置添加几个中间件,以提供良好的开发人员体验,并设置 store 以便 Redux DevTools Extension 可以检查其内容。
Redux Slices 分片
“Slice”是一个应用程序中单个功能的 Redux reducer 逻辑和 actions 的集合, 通常在单个文件中定义在一起。这个名字来自于将根 Redux state 对象拆分成多个“state slices”。
例如,store 设置可能如下所示:
import { configureStore } from "@reduxjs/toolkit";
import usersReducer from "../features/users/usersSlice";
import postsReducer from "../features/posts/postsSlice";
import commentsReducer from "../features/comments/commentsSlice";
export default configureStore({
reducer: {
users: usersReducer,
posts: postsReducer,
comments: commentsReducer,
},
});其中 usersReducer,postsReducer, commentsReducer 也被称作 slice reducer
combineReducers
在创建 Redux store 时,需要传入一个单独的“root reducer”函数。 因此,如果有许多不同的 slice reducer 函数,应该如何获得一个单一的 root reducer, 并从而定义 Redux store state 的内容呢?
手动调用:
function rootReducer(state = {}, action) {
return {
users: usersReducer(state.users, action),
posts: postsReducer(state.posts, action),
comments: commentsReducer(state.comments, action),
};
}Redux 提供了一个名为 combineReducers 的函数,可以自动地完成这个操作。 它接受一个由多个 slice reducer 组成的对象作为参数,并返回一个函数,该函数在每次派发一个 action 时调用每个 slice reducer。 每个 slice reducer 的结果都合并在一起作为最终结果的单个对象。
可以使用 combineReducers 实现与前面示例相同的功能:
const rootReducer = combineReducers({
users: usersReducer,
posts: postsReducer,
comments: commentsReducer,
});Redux Toolkit 中提供了的 configureStore 工具函数,它允许使用 createSlice 和 createAsyncThunk 创建的 reducer 直接作为参数进行传递。而不必再使用 combineReducers 函数将所有 reducer 组合成一个根 reducer。
创建 Slice Reducers 和 Actions
从前面知道,actions 是一个带有 type 字段的普通对象,type 字段总是一个字符串,通常我们会有“action creator”函数来创建并返回 action 对象。 那么这些 action 对象、type 字符串和 action creators 是在哪里定义的呢?
可以每次手写这些内容,但那会很繁琐。 此外,在 Redux 中真正重要的是 reducer 函数以及它们计算新 state 的逻辑。
Redux Toolkit 有一个叫做 createSlice 的函数,它负责生成 action type strings、action creator 函数和 action 对象。 需要做的就是为该 slice 定义一个名称,编写一个包含一些 reducer 函数的对象,它会自动生成相应的 action 代码。
name 选项中的字符串被用作每个 action type 的第一部分,每个 reducer 函数的键名被用作第二部分。 因此,“counter”名称 + “increment”reducer 函数生成了一个 action type 为{type:“counter/increment”}的对象。
// features/counter/counterSlice.js
import { createSlice } from "@reduxjs/toolkit";
export const counterSlice = createSlice({
name: "counter",
initialState: {
// 初始值
value: 0,
},
reducers: {
increment: (state) => {
// Redux Toolkit 允许编写 'mutating' 逻辑 它内部使用了 immer,
// 所以这样写不会直接改变state,而是产生一个新的state,符合redux的不可变原则
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action) => {
state.value += action.payload;
},
},
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export const selectCount = (state) => state.counter.value;
export default counterSlice.reducer;除了名称字段之外,createSlice 还需要传入 reducer 的初始 state 值,以便在第一次调用时有 state 可用。
Reducers 的规则
- 它们只应该基于 state 和 action 参数计算出新的 state 值。
- 它们不允许修改现有的 state。相反,它们必须进行不可变的更新,即复制现有的 state 并对复制值进行更改。
- 它们不能执行任何异步逻辑或其他“副作用”。
为什么这些规则如此重要呢?有一些原因:
- Redux 的目标之一是代码可预测。当一个函数的输出仅从输入参数计算时,更容易理解代码的工作原理并进行测试。
- 另一方面,如果一个函数依赖于它外部的变量,或者表现得随机,将永远不知道运行它时会发生什么。
- 如果一个函数修改其他值,包括它的参数,那么它可能会出现意外的应用程序行为。这可能是一些常见的错误源,例如,“更新了 state,但是现在 UI 没有在应该更新时更新”
- 一些 Redux DevTools 的功能需要正确遵循这些规则的 reducer,比如时间旅行。
Reducers 和 Immutable Updates
在 Redux 中,我们的 reducers 永远不允许修改原始/当前的 state 值! 有几个原因,为什么我们不能在 Redux 中突变 state:
- 它会导致 bug,如 UI 不更新以显示最新的值。
- 它使理解 state 被更新的原因和方式更加困难。
- 它使编写测试变得更加困难。
- 它破坏了正确使用“时间旅行调试”的能力。
- 这违背了 Redux 的既定精神和使用模式。 那么,如果不能改变原始值,如何返回更新后的 state 呢?
手动编写 Immutable Updates:
return {
...state,
value: 123,
};手动编写不可变更新逻辑很难,并且在 reducers 中意外突变 state 是 Redux 用户最常见的错误。
这就是为什么 Redux Toolkit 的 createSlice 函数要以更简单的方式编写不可变更新!
createSlice 在内部使用名为 Immer 的库。 Immer 使用了 Proxy 来包装提供的数据,并允许编写“突变”该包装数据的代码。 但是,Immer 跟踪尝试进行的所有更改,然后使用该更改列表返回一个安全的不可变更新的值, 就像手动编写了所有不可变更新逻辑一样。
function handwrittenReducer(state, action) {
return {
...state,
first: {
...state.first,
second: {
...state.first.second,
[action.someId]: {
...state.first.second[action.someId],
fourth: action.someValue,
},
},
},
};
}
// 使用了 immer
function reducerWithImmer(state, action) {
state.first.second[action.someId].fourth = action.someValue;
}使用 createSlice:
export const counterSlice = createSlice({
name: "counter",
initialState: {
value: 0,
},
reducers: {
increment: (state) => {
// Redux Toolkit允许我们在reducer中编写“可变”逻辑。
// 它实际上不会更改state,因为它使用immer库来检测“draft state”的更改
// 并基于这些更改生成全新的不可变state。
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action) => {
state.value += action.payload;
},
},
});使用 thunk 编写异步逻辑
JavaScript 语言有很多编写异步代码的方式,应用程序通常需要处理像从 API 获取数据之类的异步逻辑。 我们需要在 Redux 应用程序中找到一个放置异步逻辑的地方。
Thunk是一种特定类型的 Redux 函数,可以包含异步逻辑。Thunk 使用两个函数编写:
- 一个内部 thunk 函数,它有 dispatch 和 getState 作为参数
- 一个外部创造器函数,创建并返回 thunk 函数
由 counterSlice 导出的 thunk action creator 函数示例:
export const incrementAsync = (amount) => (dispatch) => {
setTimeout(() => {
dispatch(incrementByAmount(amount));
}, 1000);
};可以像使用典型的 Redux action creator 一样使用它们:
store.dispatch(incrementAsync(5));然而,使用 thunks 需要在创建 Redux store 时添加 redux-thunk 中间件(一种 Redux 的插件类型)。 幸运的是,Redux Toolkit 的 configureStore 函数已经自动为我们设置了这个中间件,因此可以在这里使用 thunks。
请求数据包含在 thunk 中:
// 外层 "thunk creator" 函数
const fetchUserById = (userId) => {
// 内层 "thunk 函数"
return async (dispatch, getState) => {
try {
// 在thunk进行异步处理
const user = await userAPI.fetchById(userId);
// 拿到请求结果后,dispatch action
dispatch(userLoaded(user));
} catch (err) {
// 错误处理
}
};
};thunk 和 异步逻辑
Redux 不允许在 reducers 中放任何的异步逻辑。
如果我们可以访问 Redux store,我们可以编写一些异步代码,并在完成后调用 store.dispatch():
const store = configureStore({ reducer: counterReducer });
setTimeout(() => {
store.dispatch(increment());
}, 250);但是,在真正的 Redux 应用程序中,不允许将 store 导入到其他文件中,特别是我们的 React 组件中,因为这会使该代码更难进行测试和重用。
此外,我们经常需要编写一些异步逻辑,我们知道它最终会与某个 store 一起使用,但我们不知道哪个 store。
Redux store 可以使用“中间件”进行扩展,这是一种可以添加额外功能的附加组件或插件。 使用中间件的最常见原因是可以编写具有异步逻辑但仍然同时与 store 通信的代码。 它们还可以修改 store,以便我们可以调用 dispatch()并传递不是普通 action 对象的值,例如函数或 Promise。
Redux Thunk 中间件修改 store,以便可以将函数传递到 dispatch 中:
// thunk
const thunkMiddleware =
({ dispatch, getState }) =>
(next) =>
(action) => {
if (typeof action === "function") {
return action(dispatch, getState);
}
return next(action);
};这段代码会检查传递到 dispatch 中的“action”是否实际上是一个函数,而不是一个普通 action 对象。 如果它实际上是一个函数,它会调用函数并返回结果。 否则,由于这是一个 action 对象,它将 action 继续传递到 store。
这样,就可以编写任何同步或异步代码,同时仍然可以访问 dispatch 和 getState。
React 组件中的使用
示例:
import React, { useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import {
decrement,
increment,
incrementByAmount,
incrementAsync,
selectCount,
} from "./counterSlice";
import styles from "./Counter.module.css";
export function Counter() {
const count = useSelector(selectCount);
const dispatch = useDispatch();
const [incrementAmount, setIncrementAmount] = useState("2");
return (
<div>
<div className={styles.row}>
<button
className={styles.button}
aria-label="Increment value"
onClick={() => dispatch(increment())}
>
+
</button>
<span className={styles.value}>{count}</span>
<button
className={styles.button}
aria-label="Decrement value"
onClick={() => dispatch(decrement())}
>
-
</button>
</div>
{/* omit additional rendering output here */}
</div>
);
}React-Redux 库提供了一组自定义钩子,允许您的 React 组件与 Redux store 进行交互。
使用 useSelector 读取数据
首先,useSelector 钩子允许我们从 Redux store 状态中提取组件所需的任何数据。
前面提到可以编写"selector"函数,它以 state 作为参数并返回 state 值的某个部分。
比如之前的 counterSlice.js 中的 selectCount:
// selector 也可以内联定义,而不是在Slice文件中使用。
// 例如:' useSelector((state) => state.counter.value) '
export const selectCount = (state) => state.counter.value;如果可以访问 store 的话就可以通过这种方式获取 store 中的 state:
const count = selectCount(store.getState());
console.log(count);
// 0但是组件不能直接与 Redux store 通信,因为 redux 不允许我们将其导入组件文件中。 useSelector 会在幕后为处理与 Redux store 的对话。 如果传入一个 someSelector 函数,它会调用 someSelector(store.getState()),并返回结果。
// 两种写法
const count = useSelector(selectCount);
const countPlusTwo = useSelector((state) => state.counter.value + 2);每当 action 被派发并且 Redux stroe 已被更新时,useSelector 将重新运行选择器函数。 如果选择器返回的值与上次不同,useSelector 将确保组件使用新值进行重新渲染。
useDispatch 调度 actions
类似地,如果我们有一个 Redux store 的访问权限,可以使用 action 创建器来派发 action,比如 store.dispatch(increment())。 但是由于没有访问 stroe 本身,所以需要某种方式仅访问 dispatch 方法。
useDispatch 钩子就完成了这项工作,并提供了 Redux stroe 的实际 dispatch 方法:
使用 useDispatch:
const dispatch = useDispatch();
<button
className={styles.button}
aria-label="Increment value"
onClick={() => dispatch(increment())}
>
+
</button>;提供 store
已经知道组件可以使用 useSelector 和 useDispatch 钩子来访问 Redux stroe。 但是,由于我们没有导入 stroe,这些钩子如何知道要访问哪个 Redux stroe?
回到这个应用程序的起点:
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import store from "./app/store";
import { Provider } from "react-redux";
ReactDOM.render(
// 传入stroe
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);我们总是需要调用 ReactDOM.render(
将
现在,任何调用 useSelector 或 useDispatch 的 React 组件都将与提供给