React-状态管理
react之状态管理
随着应用的不断加大,我们应该更有意识的的去关注我们的应用状态如何去组织,以及数据如何在组件中流动。我们应该去避免冗余或重复的状态,这样可以避免一些缺陷的产生。这时候,如何组织好状态,如何保持状态更新逻辑的可维护性,以及如何跨组件共享状态就变得尤为重要。
使用状态响应输入
在使用React时,我们不需要直接代码层面去修改UI。我们可以直接通过组件的不同状态去展现的UI,然后根据用户输入触发状态修改。
声明式UI与命令式UI的比较
当设计UI交互时,会思考UI如何根据用户的操作而响应变化。想象一个允许用户提交一个答案的表单:
- 当表单输入的时候,“提交”按钮会变成可用状态
- 点击“提交”后,表单和提交按钮都会随之变成不可用状态
- 如果网络请求成功,表单会随之隐藏,同时会出现“提交成功”提示
- 如果网络请求失败,会出现错误提示信息,表单又变为可用状态
命令式编程中,我们需要根据具体的场景去设计如何实现交互。我们必须根据可能会发生的事情去写一些明确的命令去操作UI。也就是我们需要“命令”每个元素(操作dom),告诉计算机应该如何去更新UI的编程方式被称为命令式编程。
对于独立系统来说,命令式控制用户界面的效果也不错,但是如果要实现更为复杂的系统时,代码的组织就会指数级难度增长。
而React就是为了解决这样的问题而诞生。
在React中,不比直接去操作UI(不需要去直接操作dom)。相反,我们只需要声明我们想要显示的内容。React就会通过计算如何去更新UI。
声明式考虑UI
通过上面的思考方式,我们来看看React时如何去实现这个UI。
- 定位组件中不同的视图状态
- 确定是什么触发这些
state
的改变 - 表示内存中的
state
(需要使用useState
) - 删除任何不必要的
state
变量 - 连接事件处理函数去设置
state
步骤1:定位组件中不同的视图状态
在React中,不同的可视化UI界面中用户所有看到的都是不同的“状态”。
- 无数据:表单有一个不可用状态的“提交”按钮。
- 输入中:表单有一个可用状态的“提交”按钮。
- 提交中:表单完全处于不可用状态,加载动画出现。
- 成功时:显示“成功”的消息而非表单。
- 错误时:与输入状态类似,但会多错误的消息。
步骤2:确定是什么触发了这些状态的改变
触发state
的更新来响应俩种输入:
- 人为输入。比如点击按钮、在表单中输入内容,或导航到链接。
- 计算机输入。比如网络请求得到反馈、定时器被触发,或加载一张图片。
以上两种情况中,你必须设置 state 变量 去更新 UI。对于正在开发中的表单来说,你需要改变 state 以响应几个不同的输入:
- 改变输入框中的文本时(人为)应该根据输入框的内容是否是空值,从而决定将表单的状态从空值状态切换到输入中或切换回原状态。
- 点击提交按钮时(人为)应该将表单的状态切换到提交中的状态。
- 网络请求成功后(计算机)应该将表单的状态切换到成功的状态。
- 网络请求失败后(计算机)应该将表单的状态切换到失败的状态,与此同时,显示错误信息。
步骤3:通过useState
表示内存中的state
接下来,我们会需要在内存中通过 useState
表示组件中的视图状态。state
的每个部分都是“处在变化中的”,并且需要让“变化大的部分”尽可能的少。
- 先从绝对必须存在的状态开始。
- 接下来,创建一个状态变量去代表想要显示的可时状态
- 在很难想出最好的办法时,就从添加足够多的
state
开始,确保所有可能的视图状态都包含。
最初的想法或许不是最好的,必要时,**重构state
**也是步骤中的一部分。
步骤4:删除任何不必要的state变量
我们想要避免state
内容中的重复,从而只需要关注必要的部分。这就需要我们花费时间去重构我们的state
结构,这样会让我们的组件更容易被理解,从而减少重复避免歧义。主要的目的是防止出现在内存中的state不代表任何我们让用户看到的有效UI的情况。
在创建state
变量的时候,我们应该反问自己以下这些问题:
- 这个
state
是否会导致矛盾?例如,isTyping
与isSubmitting
的状态不能同时为true
。矛盾的产生通常说明了这个 state 没有足够的约束条件。两个布尔值有四种可能的组合,但是只有三种对应有效的状态。为了将“不可能”的状态移除,可以将'typing'
、'submitting'
以及'success'
这三个中的其中一个与status
结合。 - 相同的信息是否已经在另一个 state 变量中存在?另一个矛盾:
isEmpty
和isTyping
不能同时为true
。通过使它们成为独立的 state 变量,可能会导致它们不同步并导致 bug。幸运的是,可以移除isEmpty
转而用message.length === 0
。 - 是否可以通过另一个 state 变量的相反值得到相同的信息?
isError
是多余的,因为你可以检查error !== null
。
正是因为在不破坏功能的情况下删除其中任何一个状态变量,才可以确定这些都是必要的。
步骤5:连接事件处理函数以设置state
最后,创建事件处理函数去设置state
变量。
选择状态结构
良好的状态组织,可以区分易于修改和调试的组件和频繁出问题的组件。最重要的原则是,状态不应该包含冗余或重复的信息。
构建state的原则
当编写一个存有state
的组件时,需要我们去选择使用多少个state
变量以及他们都是什么数据格式。以下是构建state
的原则来指导哦们做出更好的决策:
- **合并关联的
state
**。如果存在同时更新俩个或者多个的state
变量,就该考虑将他们合并为一个单独的state
变量。 - **避免互相矛盾的
state
**。当state
结构存在多个互相矛盾或者“不一致”的state
时,就应该避免这个情况。 - **避免冗余的
state
**。如果在渲染期间从组件的props
或现有的state
变量中计算一些信息,那么这些信息不因该放在该组件的state
中。 - **避免重复的
state
**。当统一数据在多个state
变量之间或在多个嵌套对象总重复时,会很难保持同步。因此应尽量减少重复。 - **避免深层嵌套的
state
**。深度分层的state
更新起来会不方便,最好时构建扁平化的state
。
这些原则背后的目标是使state
易于更新而不引入错误。从state
中删除冗余和重复数据有助于所有部分保持同步。这类似于数据库工程师想要 “规范化”数据库结构,以减少出现错误的机会。用爱因斯坦的话说,“让你的状态尽可能简单,但不要过于简单。”
合并关联的state
有时候我们可能不确定使用单个state
变量还是多个state
变量。
例如下面的存在x、y
是这样做?
1 | const [x, setX] = useState(0); |
还是这样做?
1 | const [position, setPosition] = useState({ x: 0, y: 0 }); |
从技术实现上来说,可以使用任何一种方法。但是,如果俩个state
变量总是一起变化,则将它们统一成state变量可能会更好。这样我们就不会忘记让它们始终保持同步。
另一种情况是,我们将数据整合到一个对象或一个数组中时,不知道需要多少个state
片段。例如,用户可以自定义字段的表单,这样就很有帮助。
注意:如果 state 变量是一个对象时,请记住,不能只更新其中的一个字段 而不显式复制其他字段。例如,在上面的例子中,不能写成 setPosition({ x: 100 })
,因为它根本就没有 y
属性! 相反,如果你想要仅设置 x
,则可执行 setPosition({ ...position, x: 100 })
,或将它们分成两个 state 变量,并执行 setX(100)
。
避免矛盾的state
看以下例子代码:
1 | import { useState } from 'react'; |
观察这段代码,其实是有效的,但是会存在一些state
“极难处理”。如果忘记 setIsSent
和 setIsSending
,则可能会出现 isSending
和 isSent
同时为 true
的情况。组件越复杂,就很难理解发生了什么。
因为 isSending
和 isSent
不应同时为 true
,所以最好用一个 status
变量来代替它们,这个 state 变量可以采取三种有效状态其中之一:'typing'
(初始), 'sending'
, 和 'sent'
:
1 | import { useState } from 'react'; |
避免冗余的state
如果我们在渲染期间从组件的props
或现有的state
变量中计算出一些信息,则不应该把这些信息放到该组件的state
中。
仔细看看以下代码。他可以允许,仔细观察就能发现其中的冗余之处。
1 | import { useState } from 'react'; |
为什么能用常量去做呢?这是因为事件处理程序不需要任何操作去更新它,当我们调用setFirstName
或 setLastName
时,你会触发一次重新渲染,然后下一个 fullName
将从新数据中计算出来。
深入探讨:不要在 state 中镜像 props
看看以下代码:
1 | function Message({ messageColor }) { |
这里,一个 color
state 变量被初始化为 messageColor
的 prop 值。这段代码的问题在于,如果父组件稍后传递不同的 messageColor
值(例如,将其从 'blue'
更改为 'red'
),则 color
state 变量将不会更新! state 仅在第一次渲染期间初始化。
这就是为什么在 state 变量中,“镜像”一些 prop 属性会导致混淆的原因。相反,你要在代码中直接使用 messageColor
属性。如果想给它起一个更短的名称,请使用常量:
1 | function Message({ messageColor }) { |
这种写法就不会与从父组件传递的属性失去同步。
只有当你 想要 忽略特定 props 属性的所有更新时,将 props “镜像”到 state 才有意义。按照惯例,prop 名称以 initial
或 default
开头,以阐明该 prop 的新值将被忽略:
1 | function Message({ initialColor }) { |
避免重复的state
下面这是一个可以选择的菜单列表:
1 | import { useState } from 'react'; |
这里的所选元素作为对象存储在selectedItem
state变量中。然而,**selecteItem
的内容与items
列表中的某一项是同一个对象**,这意味着关于该项本身的信息在俩个地方产生了重复。
我们在将我们的组件进行修改,让每个项目都可以编辑,看看会出现什么问题?
1 | import { useState } from 'react'; |
注意,当我们选择“choose”按钮时,然后编辑它,输入会更新,但是底部的标签不会反应编辑的内容。这是因为我们存在了重复的state
,并且忘记去更新了selectedItem
。
我们也可以选择去更新selectedItem
,但更简单的办法是去消除重复项。在下面对代码中,我们就可以将selectedId
保存在state中,而不是在selectedItem
对象中(它创建了一个与items
内重复的对象),然后通过items
数组去过滤出具有该ID的项,以此来获取selectedItem
:
1 | import { useState } from 'react'; |
在或者,我们可以将所选择的索引保存在state
中。
state 过去常常是这样赋值的:
items = [{ id: 0, title: 'pretzels'}, ...]
selectedItem = {id: 0, title: 'pretzels'}
改了之后是这样的:
items = [{ id: 0, title: 'pretzels'}, ...]
selectedId = 0
这样,就不存在重复的state
,只保留了必要的state
。这样的话,当我们编辑selected
元素,下面的标签就会立即更新。这是因为setItem
会触发重新渲染,而item.find(...)
会找到带有更新文本的元素,我们不需要在state中保存选定的元素,因为只有选定的ID是必要的。其余的都可以在渲染期间计算得出。
避免深度的嵌套state
想象一下,一个由行星、大陆和国家组成的旅行计划。你可能会尝试使用嵌套对象和数组来构建它的 state,就像下面这个例子:
1 | // app.js |
这时候如果我们想添加一个按钮来删除已经去过的地方。该如何做呢?更新嵌套的 state 需要从更改部分一直向上复制对象。删除一个深度嵌套的地点将涉及复制其整个父级地点链。这样的代码可能非常冗长。
如果 state 嵌套太深,难以轻松更新,可以考虑将其“扁平化”。 这里可以通过一个方法来重构上面这个数据:不同树状结构,每个节点的place
都是一个包含其子节点的数组,我们可以让每个节点的place
作为数组保存其子节点的ID。然后存储一个节点ID与相应节点的映射关系。
看看下面重组的数据:
1 | // app.js |
此刻,state 已经“扁平化”(也称为“规范化”),更新嵌套项会变得更加容易。
现在删除一个地点的话,只需要更新两个 state 级别:
- 其 父级 地点的更新版本应该从其
childIds
数组中排除已删除的 ID。 - 其根级“表”对象的更新版本应包括父级地点的更新版本。
看以下代码处理的实例:
1 | // app.js |
这样就可以随心所欲嵌套state,“扁平化”可以解决很多问题。这使得state更容易更新,确保嵌套对象的不同部分中没有重复。
在组件间共享状态
如果我们需要俩个组件的状态始终同步修改。我们可以将相关状态从这俩个组件上移除,将这些状态移到最近的父级组件,然后通过props
将这些状态传递给这俩个组件。这被称为“状态提升”。
对state进行保留和重置状态
当我们重新渲染一个组件时,React需要决定组件树哪些部分要保留和更新,以及丢弃或重新创建。在大多数情况下,React的自动处理机制已经做了大部分工作。默认情况下,React会保留树中与先前渲染组件树“匹配”的部分。
React允许覆盖默认行为,这时候可以通过向组件传递一个唯一key
来强制重置其状态。这将告诉React,组件需要重新渲染。
提取状态到reducer中
对于需要更新多个状态的组件来说,会存在过于分散的事件处理程序。对于这种情况,我们可以在组件外部将所有的状态更新逻辑合并到一个称为“reducer”的函数中。这样,事件处理程序就会变的简洁,只需要我们指定对应的“action”。同时定义,reducer
函数指定状态因该如何更新去响应每个action
!
使用Context进行深层数据传递
通常,我们会存在需要通过props
将信息从父组件传递给子组件。如果需要在组件树中深入传递prop
,或者树中许多组件都需要使用相同的prop
,那么传递prop
可能会变得麻烦。Context
允许父组件将一些信息提供给它下层的任何组件,不管组件多深层也无需通过props
逐层透传。
使用Reducer和Context进行状态扩展
Reducer可以帮助我们合并组件的状态更新逻辑。Context可以帮助我们将信息深处传递给其他组件。可以将二者结合使用,以管理复杂应用的状态。