React UI 中的声明式思维

React UI 中的声明式思维

声明式的考虑UI

声明式UI意味着不必直接启用、关闭、显示或隐藏组件,而是只需要 声明你想要显示的内容, React 就会通过计算得出该如何去更新 UI。在 React 中,可以通过如下步骤思考如何实现UI效果:

  1. 定位组件中不同的视图状态
  2. 确定是什么触发了这些 state 的改变
  3. 表示内存中的 state(需要使用 useState
  4. 删除任何不必要的 state 变量
  5. 连接事件处理函数去设置 state

定位组件中不同的视图状态

可视化 UI 界面中用户可能看到的所有不同的“状态”,例如:

  • 无数据:表单有一个不可用状态的“提交”按钮。
  • 输入中:表单有一个可用状态的“提交”按钮。
  • 提交中:表单完全处于不可用状态,加载动画出现。
  • 成功时:显示“成功”的消息而非表单。
  • 错误时:与输入状态类似,但会多错误的消息。

在添加逻辑之前去“模拟”不同的状态或创建“模拟状态”,这个过程可在书写逻辑前快速迭代 UI。
官网示例代码如下:

表单布局
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export default function Form({
status = 'empty'
}) {
if (status === 'success') {
return <h1>That's right!</h1>
}
return (
<>
<h2>City quiz</h2>
<p>
In which city is there a billboard that turns air into drinkable water?
</p>
<form>
<textarea />
<br />
<button>
Submit
</button>
</form>
</>
)
}

确定是什么触发了这些状态的改变

触发 state 的更新来响应两种输入:

  • 人为输入。比如点击按钮、在表单中输入内容,或导航到链接。
  • 计算机输入。比如网络请求得到反馈、定时器被触发,或加载一张图片。

以上两种情况中,必须设置 state 变量 去更新 UI。对于正在开发中的表单来说,需要改变 state 以响应几个不同的输入:

  • 改变输入框中的文本时(人为)应该根据输入框的内容是否是空值,从而决定将表单的状态从空值状态切换到输入中或切换回原状态。
  • 点击提交按钮时(人为)应该将表单的状态切换到提交中的状态。
  • 网络请求成功后(计算机)应该将表单的状态切换到成功的状态。
  • 网络请求失败后(计算机)应该将表单的状态切换到失败的状态,与此同时,显示错误信息。

可通过如下表示方法,对状态变化进行可视化:
表单状态转换

通过 useState 表示内存中的 state

接下来需要在内存中通过 useState表示组件中的视图状态。诀窍很简单:state 的每个部分都是“处于变化中的”,并且让“变化的部分”尽可能的少
根据上一部分的状态图,需要存储输入的 answer 以及用于存储最后一个错误的 error,这是必须的两个状态。除此之外,一开始可以添加足够多的 state 开始,确保所有可能的视图状态都囊括其中:

表单状态
1
2
3
4
5
6
7
8
9
10
// 必须状态
const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);

// 其余状态
const [isEmpty, setIsEmpty] = useState(true);
const [isTyping, setIsTyping] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [isError, setIsError] = useState(false);

删除任何不必要的 state 变量

为避免 state 内容中的重复,从而只需要关注那些必要的部分,可以对前一步中的状态进行重构。这可以让组件更容易被理解,减少重复并且避免歧义,同时防止出现在内存中的 state 不代表任何你希望用户看到的有效 UI 的情况。
可以通过如下一些问题促进对状态的思考:

  • 这个 state 是否会导致矛盾?例如,isTyping 与 isSubmitting 的状态不能同时为 true。矛盾的产生通常说明了这个 state 没有足够的约束条件。两个布尔值有四种可能的组合,但是只有三种对应有效的状态。为了将“不可能”的状态移除,可以将 'typing''submitting' 以及 'success' 这三个中的其中一个与 status 结合。
  • 相同的信息是否已经在另一个 state 变量中存在?另一个矛盾:isEmpty 和 isTyping 不能同时为 true。通过使它们成为独立的 state 变量,可能会导致它们不同步并导致 bug。幸运的是,可以移除 isEmpty 转而用 message.length === 0
  • 是否可以通过另一个 state 变量的相反值得到相同的信息isError 是多余的,因为可以检查 error !== null

重构完成以后,剩下三个必要的状态:

精简后的表单状态
1
2
3
const [answer, setAnswer] = useState('');  
const [error, setError] = useState(null);
const [status, setStatus] = useState('typing'); // 'typing', 'submitting', or 'success'

连接事件处理函数以设置 state

最后,创建事件处理函数以设置 state 变量。下面是绑定好事件的最终表单:

绑定事件处理函数后的表单示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import { useState } from 'react';

export default function Form() {
const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
const [status, setStatus] = useState('typing');

if (status === 'success') {
return <h1>That's right!</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>City quiz</h2>
<p>
In which city is there a billboard that turns air into drinkable water?
</p>
<form onSubmit={handleSubmit}>
<textarea
value={answer}
onChange={handleTextareaChange}
disabled={status === 'submitting'}
/>
<br />
<button disabled={
answer.length === 0 ||
status === 'submitting'
}>
Submit
</button>
{error !== null &&
<p className="Error">
{error.message}
</p>
}
</form>
</>
);
}

function submitForm(answer) {
// Pretend it's hitting the network.
return new Promise((resolve, reject) => {
setTimeout(() => {
let shouldError = answer.toLowerCase() !== 'lima'
if (shouldError) {
reject(new Error('Good guess but a wrong answer. Try again!'));
} else {
resolve();
}
}, 1500);
});
}

总结

总的来说,在 React 中,可从 UI 整体出发,为每个视图状态先声明 UI。在组件开发过程中,列出所有状态及其转换关系,通过 useState 将状态放置于组件内,在移除冗余状态后,利用设值函数及事件处理函数,根据事件结果调整状态变化。

作者

Jeill

发布于

2024-03-09

更新于

2024-03-17

许可协议

评论