添加交互性
屏幕上的某些内容会根据用户输入而更新。例如,点击图片图库会切换当前图片。在 React 中,随时间变化的数据称为 state。你可以向任何组件添加 state,并在需要时更新它。在本章中,你将学习如何编写能够处理交互、更新其 state 并随着时间显示不同输出的组件。
In this chapter
响应事件
React 允许你向 JSX 添加 事件处理器。事件处理器是你自己编写的函数,会在用户交互时被触发,比如点击、悬停、聚焦表单输入框等等。
像 <button> 这样的内置组件只支持浏览器内置事件,比如 onClick。不过,你也可以创建自己的组件,并为它们的事件处理器 props 赋予任何你喜欢的、特定于应用的名称。
export default function App() { return ( <Toolbar onPlayMovie={() => alert('播放中!')} onUploadImage={() => alert('正在上传!')} /> ); } function Toolbar({ onPlayMovie, onUploadImage }) { return ( <div> <Button onClick={onPlayMovie}> 播放电影 </Button> <Button onClick={onUploadImage}> 上传图片 </Button> </div> ); } function Button({ onClick, children }) { return ( <button onClick={onClick}> {children} </button> ); }
State:组件的记忆
组件通常需要根据交互来改变屏幕上的内容。在表单中输入应该更新输入框字段,点击图片轮播中的“next”应该更改显示的图片,点击“buy”会把商品放入购物车。组件需要“记住”一些东西:当前输入值、当前图片、购物车。在 React 中,这种组件特有的记忆称为 state。
你可以使用 useState Hook 为组件添加 state。Hooks 是特殊的函数,它们让组件能够使用 React 功能(state 就是其中之一)。useState Hook 允许你声明一个 state 变量。它接收初始 state,并返回一对值:当前 state,以及一个用于更新它的 state 设置函数。
const [index, setIndex] = useState(0);
const [showMore, setShowMore] = useState(false);下面是一个图片图库如何在点击时使用并更新 state:
import { useState } from 'react'; import { sculptureList } from './data.js'; export default function Gallery() { const [index, setIndex] = useState(0); const [showMore, setShowMore] = useState(false); const hasNext = index < sculptureList.length - 1; function handleNextClick() { if (hasNext) { setIndex(index + 1); } else { setIndex(0); } } function handleMoreClick() { setShowMore(!showMore); } let sculpture = sculptureList[index]; return ( <> <button onClick={handleNextClick}> 下一个 </button> <h2> <i>{sculpture.name} </i> 作者 {sculpture.artist} </h2> <h3> ({index + 1} / {sculptureList.length}) </h3> <button onClick={handleMoreClick}> {showMore ? '隐藏' : '显示'}详情 </button> {showMore && <p>{sculpture.description}</p>} <img src={sculpture.url} alt={sculpture.alt} /> </> ); }
渲染与提交
在你的组件显示到屏幕上之前,它们必须先由 React 渲染。理解这个过程的步骤将帮助你思考代码是如何执行的,以及解释它的行为。
想象你的组件是厨房里的厨师,用各种食材做出美味佳肴。在这个场景中,React 是服务员,它接收顾客的点单并把订单送到厨房。这个请求和提供 UI 的过程分为三步:
- 触发一次渲染(把食客的订单送到厨房)
- 渲染组件(在厨房里准备订单)
- 提交到 DOM(把订单端上桌)

触发 
渲染 
提交
Illustrated by Rachel Lee Nabors
状态如同快照
与普通的 JavaScript 变量不同,React 状态更像一个快照。设置它并不会改变你已经拥有的那个状态变量,而是会触发一次重新渲染。一开始这可能会让人感到惊讶!
console.log(count); // 0
setCount(count + 1); // 请求使用 1 进行重新渲染
console.log(count); // 仍然是 0!这种行为可以帮助你避免一些微妙的 bug。下面是一个小聊天应用。试着猜猜,如果你先按下“发送”,然后把收件人改成 Bob,会发生什么。五秒后 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="消息" value={message} onChange={e => setMessage(e.target.value)} /> <button type="submit">发送</button> </form> ); }
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。
console.log(score); // 0
setScore(score + 1); // setScore(0 + 1);
console.log(score); // 0
setScore(score + 1); // setScore(0 + 1);
console.log(score); // 0
setScore(score + 1); // setScore(0 + 1);
console.log(score); // 0你可以在设置状态时传入一个更新函数来修复它。注意把 setScore(score + 1) 改成 setScore(s => s + 1) 是如何修复“+3”按钮的。这让你可以排队多个状态更新。
import { useState } from 'react'; export default function Counter() { const [score, setScore] = useState(0); function increment() { setScore(s => s + 1); } return ( <> <button onClick={() => increment()}>+1</button> <button onClick={() => { increment(); increment(); increment(); }}>+3</button> <h1>Score: {score}</h1> </> ) }
更新状态中的对象
状态可以保存任何类型的 JavaScript 值,包括对象。但是你不应该直接修改保存在 React 状态中的对象和数组。相反,当你想要更新对象和数组时,需要创建一个新的对象(或者复制一个已有的对象),然后更新状态以使用那个副本。
通常,你会使用 ... 展开语法来复制你想要更改的对象和数组。例如,更新一个嵌套对象可以像这样:
import { useState } from 'react'; export default function Form() { const [person, setPerson] = useState({ name: 'Niki de Saint Phalle', artwork: { title: 'Blue Nana', city: 'Hamburg', image: 'https://react.dev/images/docs/scientists/Sd1AgUOm.jpg', } }); function handleNameChange(e) { setPerson({ ...person, name: e.target.value }); } function handleTitleChange(e) { setPerson({ ...person, artwork: { ...person.artwork, title: e.target.value } }); } function handleCityChange(e) { setPerson({ ...person, artwork: { ...person.artwork, city: e.target.value } }); } function handleImageChange(e) { setPerson({ ...person, artwork: { ...person.artwork, image: e.target.value } }); } return ( <> <label> Name: <input value={person.name} onChange={handleNameChange} /> </label> <label> Title: <input value={person.artwork.title} onChange={handleTitleChange} /> </label> <label> City: <input value={person.artwork.city} onChange={handleCityChange} /> </label> <label> Image: <input value={person.artwork.image} onChange={handleImageChange} /> </label> <p> <i>{person.artwork.title}</i> {' by '} {person.name} <br /> (位于 {person.artwork.city}) </p> <img src={person.artwork.image} alt={person.artwork.title} /> </> ); }
如果在代码中复制对象让你觉得繁琐,你可以使用像 Immer 这样的库来减少重复代码:
{ "dependencies": { "immer": "1.7.3", "react": "latest", "react-dom": "latest", "react-scripts": "latest", "use-immer": "0.5.1" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject" }, "devDependencies": {} }
在 state 中更新数组
数组也是另一种可以存储在 state 中的可变 JavaScript 对象,但应将其视为只读。和对象一样,当你想要更新存储在 state 中的数组时,需要创建一个新的数组(或者制作现有数组的副本),然后将 state 设置为使用这个新数组:
import { useState } from 'react'; const initialList = [ { id: 0, title: '大肚子', seen: false }, { id: 1, title: '月球景观', seen: false }, { id: 2, title: '兵马俑', 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>艺术愿望清单</h1> <h2>我想看的艺术作品列表:</h2> <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> ); }
如果在代码中复制数组变得繁琐,你可以使用像 Immer 这样的库来减少重复代码:
{ "dependencies": { "immer": "1.7.3", "react": "latest", "react-dom": "latest", "react-scripts": "latest", "use-immer": "0.5.1" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject" }, "devDependencies": {} }