用 State 响应输入:从声明式视角重塑 UI 状态机
在传统的前端开发中,想要让页面对用户输入做出反应,我们通常会使用“命令式(Imperative)”的方式: 寻找按钮元素 禁用按钮 寻找加载图标 显示加载图标 发送网络请求 收到报错 寻找错误文本容器 写入错误文字并重新启用按钮……
在代码较少时这似乎很直接,但在复杂界面中,命令式逻辑极其容易导致灾难性的“状态丢失”:例如网络请求失败了,但你忘记把按钮从“禁用”状态重新恢复,导致用户再也无法点击。
React 采用的是截然不同的 声明式(Declarative) 编程思维:你不用告诉计算机“怎么一步步去改 DOM”,你只需要告诉计算机“在特定状态下,界面应该长成什么样”。
1. 声明式 UI 开发五步法
面对任何交互界面,遵循以下标准五步,你会发现构建动态组件变得无比清晰:
第一步:定位组件的所有视觉状态(Identify your component’s different visual states)
在写任何 JavaScript 逻辑之前,先像设计师一样思考这个组件都有哪些模样:
- Empty(空白态):输入框为空,“提交”按钮处于禁用状态;
- Typing(输入态):输入框有字,“提交”按钮变为可用状态;
- Submitting(提交中):输入框和按钮都被禁用,显示“正在提交…”的加载图标;
- Success(成功态):表单完全隐藏,只显示“答对了!感谢参与”的大标题;
- Error(错误态):输入框保持可编辑,下方出现一串醒目的红色报错文本。
第二步:确定是什么触发了状态变更(Determine what triggers those state changes)
状态的变迁只来自两类驱动源:
- 人类输入:点击按钮、输入文本、切换选择;
- 计算机事件:网络请求成功返回、网络超时、定时器倒计时结束。
第三步:使用 useState 建模状态(Represent the state in memory with useState)
尽量从最直观的几个状态开始:
const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
const [status, setStatus] = useState('typing'); // 'typing', 'submitting', 或 'success'
第四步:消除非必要和矛盾的状态(Remove any non-essential state variables)
问自己几个问题:
- 是否存在矛盾?比如如果你同时声明了
isSubmitting和isSuccess,可能会在代码里出现它们同时为true的荒谬情况。用单一状态值status(取值为枚举值)就能彻底杜绝这种非法状态。 - 能否根据现有数据算出来?比如“答案是否为空”,直接
answer.length === 0即可,不要专门再加一个isEmpty状态!
第五步:连接事件处理函数以设置状态(Connect the event handlers to set state)
编写 handleSubmit 和 handleInputChange,在用户动作时调用 setStatus 或 setAnswer。
2. 完整实战:城市问答表单
让我们把这五步完整付诸实践:
import { useState } from 'react';
export default function Form() {
const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
const [status, setStatus] = useState('typing'); // 'typing', 'submitting', 'success'
if (status === 'success') {
return <h1>答对了!你是地理小天才!</h1>;
}
async function handleSubmit(e) {
e.preventDefault();
setStatus('submitting');
try {
await submitForm(answer);
setStatus('success');
} catch (err) {
setStatus('typing');
setError(err);
}
}
function handleTextareaChange(e) {
setAnswer(e.target.value);
}
return (
<>
<h2>城市知识竞答</h2>
<p>世界上哪座城市拥有能把空气直接变成饮用水的广告牌?</p>
<form onSubmit={handleSubmit}>
<textarea
value={answer}
onChange={handleTextareaChange}
disabled={status === 'submitting'}
/>
<br />
<button disabled={
answer.length === 0 ||
status === 'submitting'
}>
提交答案
</button>
{error !== null && (
<p style={{ color: 'red' }}>{error.message}</p>
)}
</form>
</>
);
}
function submitForm(answer) {
// 模拟异步网络验证接口
return new Promise((resolve, reject) => {
setTimeout(() => {
let shouldError = answer.toLowerCase() !== '利马';
if (shouldError) {
reject(new Error('猜错了,再试试看吧!(提示:秘鲁首都)'));
} else {
resolve();
}
}, 1500);
});
}
总结
- 告别命令式直接操作 DOM 的旧思维,拥抱声明式 UI;
- 像状态机一样思考,先穷举组件可能的所有视觉状态;
- 用
useState描述当前所处的状态,杜绝相互矛盾的非法状态; - 页面标签自动根据当前状态声明性地呈现对应外观。