用State相应输入
React控制UI的方式是声明式的。你不必直接控制UI的各个部分,只需要声明组建可以处于的不同状态,并根据用户的输入在它们之间切换。
声明式UI与命令式UI的比较
命令式UI:告诉计算机如何去更新UI的编程方式被称为命令式编程。
声明式UI:只需要声明你想要显示的内容,React就会通过计算得出该如何去更新UI。
声明式地考虑UI
步骤1:定位组件中不同的视图状态:
你需要去可视化UI界面中用户可能看到的所有不同的“状态”:
- 无数据:表单有一个不可用状态的“提交”按钮。
- 输入中:表单有一个可用状态的”提交“按钮。
- 提交中:表单完全处于不可用状态,加载动画出现。
- 成功时:显示”成功“的消息而非表单。
- 错误时:与输入状态类似,但会多错误消息。
步骤2:确定是什么出发了这些状态的改变
你可以触发state的更新来响应两种输入:
- 人为输入。比如点击按钮、在表单中输入内容·····
- 计算机输入。比如网络请求得到反馈、定时器被触发·····


以上两种情况中,你必须设置state变量去更新UI。
注意:
注意,人为输入通常需要事件处理函数!!!
步骤3:通过useState表示内存中的state
state的每个部分都是”处于变化中的”,而且你需要让变化的部分尽可能的少。更复杂的程序会产生更多的bug!
步骤4:删除任何不必要的state变量
你的目的是防止出现在内存中的state不代表任何你希望用户看到的有效UI的情况。
这有一些问题你可以问自己,关于state变量的问题:
- 这个state是否导致矛盾?
- 相同的信息是否已经在另一个state变量中存在?
- 你是否可以通过另一个state变量的相反值得到相同的信息?
步骤5:连接事件处理函数以设置state
选择State结构
构建state的原则
- 合并关联的state。
- 避免相互矛盾的state。
- 避免冗余的state。
- 避免重复的state。
- 避免深度嵌套的state。
这些原则的目标是使state易于更新而不引入错误。从state中删除冗余和重复数据有助于确保所有部分保持同步。
合并相关联的state
如果某两个state变量总是一起变化,则将它们统一成一个state变量可能更好。这样你就不会忘记让它们始终保持同步。例子:
import { useState } from 'react';
export default function MovingDot() {
const [position, setPosition] = useState({
x: 0,
y: 0
});
return (
<div
onPointerMove={e => {
setPosition({
x: e.clientX,
y: e.clientY
});
}}
style={{
position: 'relative',
width: '100vw',
height: '100vh',
}}>
<div style={{
position: 'absolute',
backgroundColor: 'red',
borderRadius: '50%',
transform: `translate(${position.x}px, ${position.y}px)`,
left: -10,
top: -10,
width: 20,
height: 20,
}} />
</div>
)
}
另一种情况是,你将数据整合到以单个对象或者一个数组中时,你不知道需要多少个state片段。例如,当你有一个用户可以添加自定义字段的表单时,这将会很有帮助。
避免矛盾的state
当你的两个state有冲突时,最好使用一个state变量来代替它们。例,isSending和isSend不应该同时为true,最好使用一个status变量来代替它们,这个state变量可以采取三种有效状态其中之一:'typing'(初始)、‘sending’,‘sent'
避免容易与的state
如何你能在渲染期间从组件的props或其现有的state变量中计算出一些信息,则不应该把这些信息放到改组件的state中。
避免重复的state
当项目中有state产生了重复,只修改了一个state中的内容,另一个state未修改,会导致渲染出来的数据不相同。
避免深度嵌套的state
如果state嵌套太深,难以轻松更新,可以考虑将其扁平化。
在组件间共享状态
举例说明状态提升
这个例子中,父组件Accordion渲染了2个独立的Panel组件。
每个Panel组件都有一个布尔值isActive,用于控制其内容是否可见。
const [isActive, setIsActive] = useState(false);
return (
<section className="panel">
<h3>{title}</h3>
{isActive ? (
<p>{children}</p>
) : (
<button onClick={() => setIsActive(true)}>
显示
</button>
)}
</section>
);
}
export default function Accordion() {
return (
<>
<h2>哈萨克斯坦,阿拉木图</h2>
<Panel title="关于">
阿拉木图人口约200万,是哈萨克斯坦最大的城市。它在 1929 年到 1997 年间都是首都。
</Panel>
<Panel title="词源">
这个名字来自于 <span lang="kk-KZ">алма</span>,哈萨克语中“苹果”的意思,经常被翻译成“苹果之乡”。事实上,阿拉木图的周边地区被认为是苹果的发源地,<i lang="la">Malus sieversii</i> 被认为是现今苹果的祖先。
</Panel>
</>
);
}
我们发现点击其中一个面板中的按钮并不会影响另外一个,他们是独立的。

都是 false,
因此它们的内容都不会显示。

中的按钮都只会更新当前
Panel 组件内的 isActive 值
假设现在你想要改变这种行为,以便在任何时候只展开一个面板。应该如何做?
要协调好这两面板,我们需要分三步将状态”提升“到他们的父组件中。
- 从子组件中移除state。
- 从父组件传递硬编码数据。
- 为共同的父组件添加state,并将其与事件处理函数一起向下传递。
第一步:从子组件中移除状态
你将把Panel组件对isActive的控制权交给他们的父组件。这意味着,父组件会将isActive作为prop传给子组件Panel。我们先从Panel组件中删除下面这一行:
const [isActive,setIsActive]=useState(false)
然后,把isActive加入Panel组件的props中:
function Panel({title, children, isActive}){
现在Panel的父组件就可以通过向下传递prop来控制isActive。但相反地,Panel组件对isActive地值没有控制权--现在完全由父组件决定。
第二步:从公共父组件传递硬编码数据
这个例子中,公共父组件是Accordion。因为它位于两个面板之上,可以控制它们地props,所以它将成为当前激活面板的”控制之源”,通过Accordion组件将硬编码值isActive(例如true)传递给两个面板:
import { useState } from 'react';
export default function Accordion() {
return (
<>
<h2>哈萨克斯坦,阿拉木图</h2>
<Panel title="关于" isActive={true}>
阿拉木图人口约200万,是哈萨克斯坦最大的城市。它在 1929 年到 1997 年间都是首都。
</Panel>
<Panel title="词源" isActive={true}>
这个名字来自于 <span lang="kk-KZ">алма</span>,哈萨克语中“苹果”的意思,经常被翻译成“苹果之乡”。事实上,阿拉木图的周边地区被认为是苹果的发源地,<i lang="la">Malus sieversii</i> 被认为是现今苹果的祖先。
</Panel>
</>
);
}
function Panel({ title, children, isActive }) {
return (
<section className="panel">
<h3>{title}</h3>
{isActive ? (
<p>{children}</p>
) : (
<button onClick={() => setIsActive(true)}>
显示
</button>
)}
</section>
);
}
第三步:为公共父组件添加状态
状态提升通常会改变原状态的数据存储类型。
这个例子中,一次只能激活一个面板。这意味着Accordion这个父组件需要记录那个面板是被激活的面板。我们可以用数字作为当前被激活Panel的索引,而不是boolean值:
const [activeIndex, setActiveIndex] = useState(0);