Reducer 是一个 **函数**,它决定了 **应用程序状态** 的 **变化**。它使用接收到的 **action** 来确定这种变化。我们有一些工具,比如 Redux,可以帮助我们在一个单一的存储中管理应用程序的状态变化,以便它们的行为保持一致。
为什么我们在谈论 reducer 时会提到 Redux?Redux 严重依赖 reducer 函数,这些函数接收先前的状态和一个 action 以执行下一个状态。
这篇文章我们将重点关注 reducer。我们的目标是让我们对 reducer 函数的工作方式感到舒适,以便我们能够看到它是如何用于更新应用程序状态的——并最终理解它们在 Redux 等状态管理器中所扮演的角色。
“状态”的含义
状态变化基于用户的交互,甚至像网络请求这样的事情。如果应用程序的状态由 Redux 管理,则更改会发生在 reducer 函数内部——这是唯一发生状态更改的地方。reducer 函数利用应用程序的 **初始状态** 和称为 **action** 的东西来确定 **新状态** 将是什么样子。
如果我们在上数学课,我们可以说
initial state + action = new state
就实际的 reducer 函数而言,它看起来像这样
const contactReducer = (state = initialState, action) => {
// Do something
}
我们从哪里得到初始状态和 action?这些是我们定义的东西。
state 参数
传递给 reducer 函数的 state
参数必须是应用程序的当前状态。在这种情况下,我们将其称为 initialState
,因为它将是第一个(也是当前的)状态,并且没有任何东西会先于它。
contactReducer(initialState, action)
假设我们应用程序的初始状态是一个空的联系人列表,我们的 action 是向列表中添加一个新的联系人。
const initialState = {
contacts: []
}
这创建了我们的 initialState
,它等于 reducer 函数所需的 state
参数。
action 参数
action
是一个对象,包含两个键及其值。reducer 中发生的 state 更新始终取决于 action.type
的值。在本场景中,我们演示了当用户尝试创建新联系人时会发生什么。因此,让我们将 action.type
定义为 NEW_CONTACT
。
const action = {
type: 'NEW_CONTACT',
name: 'John Doe',
location: 'Lagos Nigeria',
email: '[email protected]'
}
通常还有一个 payload
值,其中包含用户发送的内容,并将用于更新应用程序的状态。需要注意的是,action.type
是必需的,但 action.payload
是可选的。使用 payload
为 action 对象的外观带来了一定程度的结构。
更新状态
状态应该为 不可变
,这意味着不应该直接更改它。要创建更新的状态,我们可以使用 Object.assign
或选择 扩展运算符。
Object.assign
const contactReducer = (state, action) => {
switch (action.type) {
case 'NEW_CONTACT':
return Object.assign({}, state, {
contacts: [
...state.contacts,
action.payload
]
})
default:
return state
}
}
在上面的示例中,我们使用了 Object.assign()
来确保我们不会直接更改 state 值。相反,它允许我们返回一个新对象,该对象填充了传递给它的 state 和用户发送的 payload。
要使用 Object.assign()
,重要的是第一个参数必须是一个空对象。将 state 作为第一个参数传递会导致它被修改,这正是我们为了保持一致性而试图避免的。
扩展运算符
object.assign()
的替代方法是使用扩展运算符,如下所示
const contactReducer = (state, action) => {
switch (action.type) {
case 'NEW_CONTACT':
return {
...state, contacts:
[...state.contacts, action.payload]
}
default:
return state
}
}
这确保了传入的 state 保持不变,因为我们将新项追加到底部。
使用 switch 语句
前面我们注意到发生的更新取决于 action.type
的值。switch 语句根据 action.type
的值有条件地确定我们正在处理的更新类型。
这意味着一个典型的 reducer 会像这样
const addContact = (state, action) => {
switch (action.type) {
case 'NEW_CONTACT':
return {
...state, contacts:
[...state.contacts, action.payload]
}
case 'UPDATE_CONTACT':
return {
// Handle contact update
}
case 'DELETE_CONTACT':
return {
// Handle contact delete
}
case 'EMPTY_CONTACT_LIST':
return {
// Handle contact list
}
default:
return state
}
}
当 action 对象中指定的 action.type
值与 reducer 中的值不匹配时,我们必须在 default
中返回 state——例如,如果由于某种未知原因,action 看起来像这样
const action = {
type: 'UPDATE_USER_AGE',
payload: {
age: 19
}
}
由于我们没有这种 action 类型,因此我们希望返回 state 中的内容(应用程序的当前状态)。这意味着我们目前不确定用户想要实现什么。
整合所有内容
这是一个关于我在 React 中如何实现 reducer 函数的简单示例。
查看 CodePen 上 Kingsley Silas Chijioke (@kinsomicrote) 编写的
reducer 示例。
在 CodePen 上。
您可以看到我没有使用 Redux,但这与 Redux 使用 reducer 存储和更新状态更改的方式非常相似。主要的状态更新发生在 reducer 函数中,它返回的值设置了应用程序的更新状态。
想尝试一下吗?您可以扩展 reducer 函数以允许用户更新联系人的年龄。我希望看到您在评论区分享您的成果!
了解 reducer 在 Redux 中扮演的角色,应该可以帮助您更好地理解幕后发生的事情。如果您有兴趣阅读更多关于在 Redux 中使用 reducer 的内容,值得查看 官方文档。
我对以下问题感到好奇
如何在大型应用程序中组织多个 reducer?
是否所有状态都应该进入 redux 状态?或者是否有办法在 redux 和本地状态之间进行平衡混合?哪些是应该做和不应该做的?
您是否知道任何使用 Redux 编写的开源大型应用程序示例?
就我个人而言(使用 Vue/Vuex,但状态的概念类似)。
任何可能被认为是全局的东西(用户帐户信息、通知等)都进入存储。
任何不是“全局”但由 2 个或更多组件共享(而不是直接的父子关系)的东西也进入存储。
任何完全包含在单个组件中的内容都进入本地状态。
对于任何超出这些类别的事物,胎儿体位和 30 分钟的哭泣疗法就能解决问题。
您可以创建多个 reducer,按应用程序中的逻辑部分划分。Redux 提供了一个 combineReducers 方法,该方法的功能完全符合其名称。
您可以访问一个更大的 React 网站,并使用浏览器的 Redux 开发者扩展来查看其他网站如何拆分它们的 reducer。
另外,我非常喜欢将大部分数据存储在 Redux 中,因为它可以用作缓存。如果本地存储数据,它将在 cDU 上丢失。你可以使用 localStorage 进行缓存,但在 Redux 中,数据会在路由更改之间保留。
这取决于个人喜好。我在一家 React 代理公司工作,我们一直在讨论这个问题。
这是一篇非常好的文章。我喜欢你如何将 reducer 函数独立分解,而不是专门针对 React 的 useReducer 或 Redux 的上下文。这篇文章填补了一个真正的空白,我将与一些正在努力理解 reducer 上下文的团队成员分享它。所以对此表示衷心的感谢。
但是,我确实有一个小小的争议点。依赖于 action.type 是最佳实践和常见约定,但理论上可以用任何属性甚至基于第二个 action 参数的 switch 语句来替换。我之所以提出这一点,是因为在学习 reducer 时,我最难克服的一件事就是学习哪些是其工作所需的教条要求,哪些是常见约定。
除了我的小争议之外,我还提供了一种处理 action type 的不同方法。
使用对象而不是 switch 语句可以帮助处理 switch 的一些笨拙方面(任何嵌套的条件逻辑,在更复杂的情况下使用 let 和 const 等)。