在 React 中,随时间变化的数据叫做状态(state)。可以向任意组件添加 state,根据需要更新它。
处理事件
在 JSX 中可以使用事件处理函数。对于原生组件(比如 <button> 等),只能添加浏览器内置事件(比如 onClick)。对于自定义组件,可以使用任意自定义事件处理函数。
export default function App() {
return (
<Toolbar
onPlayMovie={() => alert('Playing!')}
onUploadImage={() => alert('Uploading!')}
/>
);
}
function Toolbar({ onPlayMovie, onUploadImage }) {
return (
<div>
<Button onClick={onPlayMovie}>
Play Movie
</Button>
<Button onClick={onUploadImage}>
Upload Image
</Button>
</div>
);
}
function Button({ onClick, children }) {
return (
<button onClick={onClick}>
{children}
</button>
);
}
State:组件的记忆
可以使用 useState Hook 给组件添加 state。Hooks 是一种特殊函数,可以让组件使用 React 特性(state 就是这些特性之一)。使用 useState 声明一个 state 变量。它接收一个初始值,返回一对值:当前 state 和一个设置 state 的函数。
const [index, setIndex] = useState(0);
const [showMore, setShowMore] = useState(false);
渲染和提交
组件要经过 React 渲染,才能在屏幕上显示。理解这个过程的步骤,会帮助你理清代码执行细节,并解释它的行为。
假设你的组件是厨师,可以把配料变大餐。那么,React 就是服务生,他负责收集食客订单和上菜。完整流程分为三个阶段:
- 触发渲染(将订单带到厨房)
- 渲染组件(从厨房拿食物)
- 提交至 DOM(将食物放到餐桌)
状态就是快照
与普通的 JavaScript 变量不同,React 状态更像一种快照。设置状态不会改变已有的状态变量,而是会触发一次重新渲染。一开始可能会难以理解。
console.log(count); // 0
setCount(count + 1); // 使用 1 请求重新渲染
console.log(count); // 依然是 0!
React 这么做可以帮助你避免某些难以察觉的错误。在下面的聊天程序中,如果你先点击“Send” 按钮,接着把收件人改为 Bob,那么 5 秒钟之后,alert 弹窗会显示哪个姓名呢?
import { useState } from 'react';
export default function Form() {
const [to, setTo] = useState('Alice');
const [message, setMessage] = useState('Hello');
function handleSubmit(e) {
e.preventDefault();
setTimeout(() => {
alert(`You said ${message} to ${to}`);
}, 5000);
}
return (
<form onSubmit={handleSubmit}>
<label>
To:{' '}
<select
value={to}
onChange={e => setTo(e.target.value)}>
<option value="Alice">Alice</option>
<option value="Bob">Bob</option>
</select>
</label>
<textarea
placeholder="Message"
value={message}
onChange={e => setMessage(e.target.value)}
/>
<button type="submit">Send</button>
</form>
);
}
排队一批状态变更
下面这个组件有 bug:点击 "+3" 只增加一次分值。
import { useState } from 'react';
export default function Counter() {
const [score, setScore] = useState(0);
function increment() {
setScore(score + 1);
}
return (
<>
<button onClick={() => increment()}>+1</button>
<button onClick={() => {
increment();
increment();
increment();
}}>+3</button>
<h1>Score: {score}</h1>
</>
);
}
状态就是快照解释了原因。设置状态会触发一次重新渲染,但是不会改变当前的状态值。因此执行 setScore(score + 1) 后,score 依然是 0。
如果想修复这个问题,可以设置状态时传入 updater 函数。注意,现在不是 setScore(score + 1),而是 setScore(s => s + 1),可以修复 "+3" 按钮。当你要批量处理多个状态变更时,这个很有用。
function increment() {
- setScore(score + 1);
+ setScore(s => s + 1);
}
更新状态中的对象
状态可以保存任意类型的 JS 值,包括对象。但是你不能直接修改 React 状态中的对象和数组。当你需要更新对象和数组时,需要创建一个新值(或创建一个现存对象的副本),然后使用新值更新状态。
import { useState } from 'react';
export default function Form() {
const [person, setPerson] = useState({
name: 'Tony Stark',
artwork: {
title: 'IronMan'
}
});
function handleNameChange(e) {
setPerson({
...person,
name: e.target.value,
});
}
function handleTitleChange(e) {
setPerson({
...person,
artwork: {
...person.artwork,
title: e.target.value,
}
});
}
return (
<>
<label>
Name:
<input
value={person.name}
onChange={handleNameChange}
/>
</label>
<label>
Title:
<input
value={person.artwork.title}
onChange={handleTitleChange}
/>
</label>
</>
);
}
如果拷贝对象的代码太多,可以使用第三方库(Immer)减少重复代码。
import { useImmer } from 'use-immer';
export default function Form() {
const [person, updatePerson] = useImmer({
name: 'Tony Stark',
artwork: {
title: 'IronMan'
}
});
function handleNameChange(e) {
updatePerson(draft => {
draft.name = e.target.value;
});
}
function handleTitleChange(e) {
updatePerson(draft => {
draft.artwork.title = e.target.value;
});
}
// ...
}
更新状态的数组
数组是另一种可以存储在状态中的可变对象,需要当作只读类型处理。就像对象一样,当更新数组时,需要创建新的副本更新状态。
import { useState } from 'react';
let nextId = 3;
const initialList = [
{ id: 0, title: 'Big Bellies', seen: false },
{ id: 1, title: 'Lunar Landscape', seen: false },
{ id: 2, title: 'Terracotta Army', seen: true },
];
export default function BucketList() {
const [list, setList] = useState(initialList);
function handleToggle(artworkId, nextSeen) {
setList(list.map(artwork => {
if (artwork.id === artworkId) {
return { ...artwork, seen: nextSeen };
} else {
return artwork;
}
}));
}
return (
<>
<h1>My list of art to see:</h1>
<ItemList
artworks={list}
onToggle={handleToggle}
/>
</>
);
}
function ItemList({ artworks, onToggle }) {
return (
<ul>
{artworks.map(artwork => (
<li key={artwork.id}>
<label>
<input
type="checkbox"
checked={artwork.seen}
onChange={e => {
onToggle(
artwork.id,
e.target.checked
);
}}
/>
{artwork.title}
</label>
</li>
))}
</ul>
);
}