添加交互
屏幕上的某些内容会响应用户输入而更新。 例如,单击画廊会切换活跃图像。 在 React 中,随时间变化的数据称为状态。 您可以向任何组件添加状态,并根据需要更新它。 在本章中,您将学习如何编写组件来处理交互、更新它们的状态以及随时间显示不同的输出。
本章内容预告
- 如何处理用户发起的事件
- 如何让组件“记住”状态信息
- React 如何分两个阶段更新 UI
- 为什么状态在您更改后不会立即更新
- 如何排队多个状态更新
- 如何更新状态中的对象
- 如何更新状态数组
响应事件
React 允许您向 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>
);
}
准备好学习这个主题了吗?
阅读响应事件以了解如何添加事件处理程序。
状态:组件的内存
作为交互的结果,组件通常需要更改屏幕上的内容。 在表单中输入应该更新输入字段,在图像轮播上单击“下一步”应该更改显示的图像,单击“购买”将产品放入购物车。 组件需要“记住”一些东西:当前输入值、当前图像、购物车。 在 React 中,这种特定于组件的内存称为状态。
您可以使用 useState Hook 将状态添加到组件。 钩子是让你的组件使用 React 功能的特殊功能(状态是这些功能之一)。 useState Hook 让你声明一个状态变量。 它获取初始状态并返回一对值:当前状态和一个允许您更新它的状态设置函数。
const [index, setIndex] = useState(0);
const [showMore, setShowMore] = useState(false);
以下是图片画廊如何在点击时使用和更新状态:
data.js
export const sculptureList = [{
name: 'Homenaje a la Neurocirugía',
artist: 'Marta Colvin Andrade',
description: 'Although Colvin is predominantly known for abstract themes that allude to pre-Hispanic symbols, this gigantic sculpture, an homage to neurosurgery, is one of her most recognizable public art pieces.',
url: 'https://i.imgur.com/Mx7dA2Y.jpg',
alt: 'A bronze statue of two crossed hands delicately holding a human brain in their fingertips.'
}, {
name: 'Floralis Genérica',
artist: 'Eduardo Catalano',
description: 'This enormous (75 ft. or 23m) silver flower is located in Buenos Aires. It is designed to move, closing its petals in the evening or when strong winds blow and opening them in the morning.',
url: 'https://i.imgur.com/ZF6s192m.jpg',
alt: 'A gigantic metallic flower sculpture with reflective mirror-like petals and strong stamens.'
}, {
name: 'Eternal Presence',
artist: 'John Woodrow Wilson',
description: 'Wilson was known for his preoccupation with equality, social justice, as well as the essential and spiritual qualities of humankind. This massive (7ft. or 2,13m) bronze represents what he described as "a symbolic Black presence infused with a sense of universal humanity."',
url: 'https://i.imgur.com/aTtVpES.jpg',
alt: 'The sculpture depicting a human head seems ever-present and solemn. It radiates calm and serenity.'
}, {
name: 'Moai',
artist: 'Unknown Artist',
description: 'Located on the Easter Island, there are 1,000 moai, or extant monumental statues, created by the early Rapa Nui people, which some believe represented deified ancestors.',
url: 'https://i.imgur.com/RCwLEoQm.jpg',
alt: 'Three monumental stone busts with the heads that are disproportionately large with somber faces.'
}, {
name: 'Blue Nana',
artist: 'Niki de Saint Phalle',
description: 'The Nanas are triumphant creatures, symbols of femininity and maternity. Initially, Saint Phalle used fabric and found objects for the Nanas, and later on introduced polyester to achieve a more vibrant effect.',
url: 'https://i.imgur.com/Sd1AgUOm.jpg',
alt: 'A large mosaic sculpture of a whimsical dancing female figure in a colorful costume emanating joy.'
}, {
name: 'Ultimate Form',
artist: 'Barbara Hepworth',
description: 'This abstract bronze sculpture is a part of The Family of Man series located at Yorkshire Sculpture Park. Hepworth chose not to create literal representations of the world but developed abstract forms inspired by people and landscapes.',
url: 'https://i.imgur.com/2heNQDcm.jpg',
alt: 'A tall sculpture made of three elements stacked on each other reminding of a human figure.'
}, {
name: 'Cavaliere',
artist: 'Lamidi Olonade Fakeye',
description: "Descended from four generations of woodcarvers, Fakeye's work blended traditional and contemporary Yoruba themes.",
url: 'https://i.imgur.com/wIdGuZwm.png',
alt: 'An intricate wood sculpture of a warrior with a focused face on a horse adorned with patterns.'
}, {
name: 'Big Bellies',
artist: 'Alina Szapocznikow',
description: "Szapocznikow is known for her sculptures of the fragmented body as a metaphor for the fragility and impermanence of youth and beauty. This sculpture depicts two very realistic large bellies stacked on top of each other, each around five feet (1,5m) tall.",
url: 'https://i.imgur.com/AlHTAdDm.jpg',
alt: 'The sculpture reminds a cascade of folds, quite different from bellies in classical sculptures.'
}, {
name: 'Terracotta Army',
artist: 'Unknown Artist',
description: 'The Terracotta Army is a collection of terracotta sculptures depicting the armies of Qin Shi Huang, the first Emperor of China. The army consisted of more than 8,000 soldiers, 130 chariots with 520 horses, and 150 cavalry horses.',
url: 'https://i.imgur.com/HMFmH6m.jpg',
alt: '12 terracotta sculptures of solemn warriors, each with a unique facial expression and armor.'
}, {
name: 'Lunar Landscape',
artist: 'Louise Nevelson',
description: 'Nevelson was known for scavenging objects from New York City debris, which she would later assemble into monumental constructions. In this one, she used disparate parts like a bedpost, juggling pin, and seat fragment, nailing and gluing them into boxes that reflect the influence of Cubism’s geometric abstraction of space and form.',
url: 'https://i.imgur.com/rN7hY6om.jpg',
alt: 'A black matte sculpture where the individual elements are initially indistinguishable.'
}, {
name: 'Aureole',
artist: 'Ranjani Shettar',
description: 'Shettar merges the traditional and the modern, the natural and the industrial. Her art focuses on the relationship between man and nature. Her work was described as compelling both abstractly and figuratively, gravity defying, and a "fine synthesis of unlikely materials."',
url: 'https://i.imgur.com/okTpbHhm.jpg',
alt: 'A pale wire-like sculpture mounted on concrete wall and descending on the floor. It appears light.'
}, {
name: 'Hippos',
artist: 'Taipei Zoo',
description: 'The Taipei Zoo commissioned a Hippo Square featuring submerged hippos at play.',
url: 'https://i.imgur.com/6o5Vuyu.jpg',
alt: 'A group of bronze hippo sculptures emerging from the sett sidewalk as if they were swimming.'
}];
App.js
import { useState } from 'react';
import { sculptureList } from './data.js';
export default function Gallery() {
const [index, setIndex] = useState(0);
const [showMore, setShowMore] = useState(false);
function handleNextClick() {
setIndex(index + 1);
}
function handleMoreClick() {
setShowMore(!showMore);
}
let sculpture = sculptureList[index];
return (
<>
<button onClick={handleNextClick}>
Next
</button>
<h2>
<i>{sculpture.name} </i>
by {sculpture.artist}
</h2>
<h3>
({index + 1} of {sculptureList.length})
</h3>
<button onClick={handleMoreClick}>
{showMore ? 'Hide' : 'Show'} details
</button>
{showMore && <p>{sculpture.description}</p>}
<img
src={sculpture.url}
alt={sculpture.alt}
/>
</>
);
}
准备好学习这个主题了吗?
阅读状态:组件的内存以了解如何记住一个值并在交互时更新它。
渲染和提交
在你的组件显示在屏幕上之前,它们必须被 React 渲染。 了解此过程中的步骤将帮助您思考代码的执行方式并解释其行为。
想象一下,您的组件是厨房里的厨师,用食材烹制美味佳肴。 在这个场景中,React 是一个服务员,负责接收客户的请求并为他们带来订单。 这个请求和提供 UI 的过程分为三个步骤:
-
Trigger
触发渲染(将用餐者的订单送到厨房) -
Render
渲染组件(在厨房准备订单) -
Commit
提交给 DOM(在表格上下订单)
准备好学习这个主题了吗?
阅读渲染和提交以了解 UI 更新的生命周期。
状态作为快照
与常规 JavaScript 变量不同,React 状态的行为更像是快照。 设置它不会更改您已有的状态变量,而是会触发重新渲染。 起初这可能令人惊讶!
console.log(count); // 0
setCount(count + 1); // Request a re-render with 1
console.log(count); // Still 0!
React 以这种方式工作,以帮助您避免细微的错误。 这是一个小聊天应用程序。 尝试猜测如果您先按“发送”然后将收件人更改为 Bob 会发生什么。 谁的名字会在五秒后出现在警报中?
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>
);
}
准备好学习这个主题了吗?
阅读状态作为快照以了解为什么状态在事件处理程序中显示为“固定”且不变。
对一系列状态更新进行排队
这个组件有问题:点击“+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。
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
您可以通过在设置状态时传递更新程序函数来解决此问题。 注意如何修复“+3”按钮,用 setScore(s => s + 1) 替换 setScore(score + 1) 。 如果您需要对多个状态更新进行排队,这会很方便。
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>
</>
)
}
准备好学习这个主题了吗?
阅读**对一系列状态更新进行排队,了解如何在下一次渲染之前对多个更新进行排队。
更新状态中的对象
State 可以保存任何类型的 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://i.imgur.com/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 />
(located in {person.artwork.city})
</p>
<img
src={person.artwork.image}
alt={person.artwork.title}
/>
</>
);
}
如果在代码中复制对象变得乏味,您可以使用像 Immer 这样的库来减少重复代码:
package.json
{
"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": {}
}
app.js
import { useImmer } from 'use-immer';
export default function Form() {
const [person, updatePerson] = useImmer({
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
}
});
function handleNameChange(e) {
updatePerson(draft => {
draft.name = e.target.value;
});
}
function handleTitleChange(e) {
updatePerson(draft => {
draft.artwork.title = e.target.value;
});
}
function handleCityChange(e) {
updatePerson(draft => {
draft.artwork.city = e.target.value;
});
}
function handleImageChange(e) {
updatePerson(draft => {
draft.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 />
(located in {person.artwork.city})
</p>
<img
src={person.artwork.image}
alt={person.artwork.title}
/>
</>
);
}
准备好学习这个主题了吗?
阅读更新状态中的对象以了解如何正确更新对象。
更新状态中的数组
数组是另一种类型的可变 JavaScript 对象,您可以将其存储在状态中,并应将其视为只读。 就像对象一样,当你想更新存储在 state 中的数组时,你需要创建一个新数组(或复制现有数组),然后设置 state 以使用新数组:
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>Art Bucket List</h1>
<h2>My list of art to see:</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 这样的库来减少重复代码:
import { useState } from 'react';
import { useImmer } from 'use-immer';
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, updateList] = useImmer(initialList);
function handleToggle(artworkId, nextSeen) {
updateList(draft => {
const artwork = draft.find(a =>
a.id === artworkId
);
artwork.seen = nextSeen;
});
}
return (
<>
<h1>Art Bucket List</h1>
<h2>My list of art to see:</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>
);
}
准备好学习这个主题了吗?
阅读更新状态中的数组了解如何正确更新数组。
网友评论