React + TypeScript: 以快照的形式呈现状态
React官方网站的文档已于2023年3月16日进行了修订(请参阅“Introducing react.dev”)。本文摘要整理了基本解释中的“State as a Snapshot”并添加了TypeScript代码。但是,我们省略了针对初学者的JavaScript基础说明。
此外,关于本系列解说的其他文章,请参考「React + TypeScript:学习React官方文档基础解说『Learn React』」。
状态变量可能会让人以为它们像标准JavaScript变量一样可以读写值。然而,状态的行为更接近于“快照”(作者注:记录当前信息的快照)。状态的设置并不是对预先确定的值进行更改,而是重新触发再渲染。
根据状态设定启动渲染
在React中,用户界面不会直接根据交互事件等进行更新。当状态发生改变时,React会重新渲染组件。相反,要显示与事件相应的界面,需要更新状态。
例如,在以下的代码示例中,当点击表单(Form)的[发送]按钮时,状态变量(isSent)会被更新,React会重新渲染UI(示例001)。
import { ChangeEventHandler, FormEventHandler, useState } from 'react';
import { Form } from './Form';
import { SendMessage } from './SendMessage';
const sendMessage = (message: string) => {
// ...
};
export default function App() {
const [isSent, setIsSent] = useState(false);
const [message, setMessage] = useState<any>('Hi!');
const handleChange: ChangeEventHandler<HTMLTextAreaElement> = ({
target: { value }
}) => {
setMessage(value);
};
const handleSubmit: FormEventHandler<HTMLFormElement> = (event) => {
event.preventDefault();
setIsSent(true);
sendMessage(message);
};
return isSent ? (
<SendMessage />
) : (
<Form
handleChange={handleChange}
handleSubmit={handleSubmit}
message={message}
/>
);
}
import { ChangeEventHandler, FC, FormEventHandler } from 'react';
type Props = {
handleChange: ChangeEventHandler<HTMLTextAreaElement>;
handleSubmit: FormEventHandler<HTMLFormElement>;
message: string;
};
export const Form: FC<Props> = ({ handleChange, handleSubmit, message }) => {
return (
<form onSubmit={handleSubmit}>
<textarea placeholder="Message" value={message} onChange={handleChange} />
<button type="submit">Send</button>
</form>
);
};
React + TypeScript: 以快照形式管理状态的示例001
让我们按顺序逐步确认点击按钮后发生了什么。
-
- 首先会调用onSubmit事件处理程序。
-
- 在那里执行的是状态变量设置函数setIsSent。新的值(true)将被加入到渲染队列中。
- 然后React会根据已更新的状态变量值(isSent)重新渲染组件。
渲染是在那个时刻拍摄的快照。
「渲染」是指React调用函数组件。该函数返回的JSX可以说是当前UI的快照。属性、事件处理程序和局部变量都根据渲染时的状态进行计算。
返回的UI快照是交互式的。因此,它包含了根据输入决定触发什么的事件处理等逻辑。因此,React会根据这个快照更新屏幕,并将其连接到事件处理程序上。结果是,例如,当按下按钮时,JSX的点击处理程序会被调用。
以下是使用React进行组件重新渲染的步骤:
-
- 函数执行:React再次调用函数。
计算快照:函数返回的是新的JSX快照。
更新DOM树:React根据返回值的快照来更新界面。
对于组件来说,与通常的变量不同,作为组件的记忆角色的状态不会在函数处理完后消失。实际上,这些状态是由React本身持有在函数外部。当React调用组件时,获取到的是应该用于渲染的状态快照。而组件返回的UI快照包含了最新的属性和事件处理器,它们都包含在JSX中。所有这些都是基于该渲染的状态值进行计算的。
为了理解状态快照,让我们考虑下面代码的结果。在按钮(
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button
onClick={() => {
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
alert(number);
}}
>
+3
</button>
</>
);
}
当我第一次点击按钮并查看结果时,显示的计数器状态变量值(number)为1。也就是说,只增加了从初始值0到1的加法。而且,警告对话框中显示状态变量值为0。
在渲染时设置的状态变量只是用于下一次渲染的值。渲染时会以最近的状态变量值(0)为基础进行渲染。因此,无论通过设置函数(setNumber)对状态变量(number)进行多少次累加,它都不会改变,仍然保持最近的值不变。
时间的流逝与状态
在上述的代码示例中,状态变量保持初始值(0)可能是因为立即打开警告对话框的原因。因此,我们可以将其改写为稍微延迟一些时间再进行确认。
export default function Counter() {
return (
<>
<h1>{number}</h1>
<button
onClick={() => {
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
// alert(number);
setTimeout(() => {
alert(number);
}, 3000);
}}
>
+3
</button>
</>
);
}
然而,显示的值仍然保持初始值不变。当React通过调用组件来获取UI的快照时,状态值会被“固定”。即使将代码设为异步,状态变量的值在该渲染期间也不会改变。
在设置状态变量时延迟一下,看看会怎样。我们将在下面的代码示例(Sample 002)中进行确认。
import { useState } from 'react';
import './styles.css';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button
onClick={() => {
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
setTimeout(() => {
setNumber(number + 10);
}, 1000);
setTimeout(() => {
alert(number);
}, 3000);
}}
>
+3
</button>
</>
);
}
样例002 ■ React + TypeScript: 状态作为快照02
画面上,计数器的数值会稍有延迟地变化(从1到10)。然而,警告对话框中显示的仍然是初始值(0)。此外,设置后的计数器值(10)作为状态变量值(number)被引用时也可以看出其是初始值。
在React中,对于一个渲染的事件处理程序,它会“固定”状态变量的值。这意味着在代码执行期间不必担心状态的变化。
总结
在本文中,我对以下几个项目进行了解释。
-
- 状態を設定すると新たなレンダーが求められます。
-
- Reactが状態を保持するのは関数の外です。
useStateを呼び出すと、Reactからそのレンダーにおける状態のスナップショットが得られます。
変数やイベントハンドラは再レンダー間で同じではありません。レンダーごとに備わるのはそれぞれ独自のイベントハンドラです。
各レンダー(およびその中の関数)はつねに、Reactがそのレンダーに与えた状態のスナップショットを参照します。
イベントハンドラが参照するのはそれがつくられたレンダーの状態変数値です。