组件通常需要根据交互来改变屏幕上显示的内容。向表单中输入内容应该更新输入字段,在图片轮播中点击“下一张”应该切换显示的图片,点击“购买”应该把商品加入购物车。组件需要“记住”一些东西:当前的输入值、当前的图片、购物车。在 React 中,这种组件特有的记忆被称为 state。
You will learn
- 如何使用
useStateHook 添加状态变量 useStateHook 返回的是哪一对值- 如何添加一个以上的状态变量
- 为什么 state 被称为局部状态
当普通变量不够用时
这里有一个渲染雕塑图片的组件。点击“Next”按钮应该通过将 index 改为 1、然后 2,依此类推,来显示下一个雕塑。不过,这不会起作用(你可以试试看!):
import { sculptureList } from './data.js'; export default function Gallery() { let index = 0; function handleClick() { index = index + 1; } let sculpture = sculptureList[index]; return ( <> <button onClick={handleClick}> Next </button> <h2> <i>{sculpture.name} </i> by {sculpture.artist} </h2> <h3> ({index + 1} of {sculptureList.length}) </h3> <img src={sculpture.url} alt={sculpture.alt} /> <p> {sculpture.description} </p> </> ); }
handleClick 事件处理函数正在更新一个局部变量 index。但有两个原因导致这个变化不会显示出来:
- 局部变量不会在渲染之间保留。 当 React 第二次渲染这个组件时,它会从头开始渲染——不会考虑任何对局部变量所做的更改。
- 对局部变量的更改不会触发渲染。 React 不会意识到它需要使用新数据再次渲染该组件。
要用新数据更新组件,需要发生两件事:
- 在渲染之间保留 数据。
- 触发 React 使用新数据渲染组件(重新渲染)。
useState Hook 提供了这两件事:
- 一个用于在渲染之间保留数据的状态变量。
- 一个用于更新该变量并触发 React 再次渲染组件的状态设置函数。
添加状态变量
要添加一个状态变量,请先在文件顶部从 React 导入 useState:
import { useState } from 'react';然后,将这一行:
let index = 0;替换为
const [index, setIndex] = useState(0);index 是状态变量,setIndex 是设置函数。
这里的
[和]语法称为 数组解构,它让你可以从数组中读取值。useState返回的数组始终正好有两个项目。
它们在 handleClick 中是这样协同工作的:
function handleClick() {
setIndex(index + 1);
}现在点击“Next”按钮会切换当前的雕塑:
import { useState } from 'react'; import { sculptureList } from './data.js'; export default function Gallery() { const [index, setIndex] = useState(0); function handleClick() { setIndex(index + 1); } let sculpture = sculptureList[index]; return ( <> <button onClick={handleClick}> Next </button> <h2> <i>{sculpture.name} </i> by {sculpture.artist} </h2> <h3> ({index + 1} of {sculptureList.length}) </h3> <img src={sculpture.url} alt={sculpture.alt} /> <p> {sculpture.description} </p> </> ); }
认识你的第一个 Hook
在 React 中,useState,以及任何以 “use” 开头的其他函数,都被称为 Hook。
Hook 是特殊的函数,只能在 React 渲染 时使用(我们会在下一页更详细地讨论这一点)。它们让你“挂接”到不同的 React 特性上。
State 只是这些特性之一,不过你稍后还会认识其他 Hook。
useState 的结构
当你调用 useState 时,你是在告诉 React,你希望这个组件记住一些东西:
const [index, setIndex] = useState(0);在这个例子中,你希望 React 记住 index。
useState 唯一的参数是你的状态变量的初始值。在这个例子中,index 的初始值通过 useState(0) 被设为 0。
每次组件渲染时,useState 都会给你一个包含两个值的数组:
- 状态变量(
index),其值就是你存储的值。 - 状态设置函数(
setIndex),它可以更新状态变量并触发 React 再次渲染组件。
下面是它在实际中的工作方式:
const [index, setIndex] = useState(0);- 你的组件第一次渲染。 因为你把
0作为index的初始值传给了useState,它会返回[0, setIndex]。React 记住0是最新的状态值。 - 你更新状态。 当用户点击按钮时,会调用
setIndex(index + 1)。index是0,所以这就是setIndex(1)。这告诉 React 现在要记住index是1,并触发另一次渲染。 - 组件的第二次渲染。 React 仍然看到的是
useState(0),但因为 React 记住 了你把index设为1,所以它返回的是[1, setIndex]。 - 以此类推!
为组件设置多个状态变量
你可以在一个组件中拥有任意数量、任意类型的状态变量。这个组件有两个状态变量,一个数字 index 和一个布尔值 showMore,当你点击“显示详情”时它会切换:
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}> 下一个 </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} /> </> ); }
如果某些状态彼此无关,比如这个例子中的 index 和 showMore,那么拥有多个状态变量是个好主意。但如果你发现自己经常需要同时修改两个状态变量,那么把它们合并成一个可能会更容易。例如,如果你有一个包含许多字段的表单,那么拥有一个保存对象的单一状态变量,比为每个字段分别设置状态变量更方便。阅读 选择状态结构 以获取更多提示。
Deep Dive
你可能已经注意到,useState 调用并没有接收任何关于它所引用的是哪个状态变量的信息。传给 useState 的并没有“标识符”,那它是怎么知道要返回哪个状态变量的呢?它依赖于解析你的函数之类的魔法吗?答案是否定的。
相反,为了保持其简洁的语法,Hooks 依赖于同一个组件每次渲染时稳定的调用顺序。 这在实践中非常有效,因为如果你遵循上面的规则(“只在顶层调用 Hooks”),Hooks 总会以相同的顺序被调用。此外,linter 插件 可以捕获大多数错误。
在内部,React 为每个组件维护一个状态对数组。它还维护当前的配对索引,在渲染前该索引被设置为 0。每次你调用 useState,React 都会给你下一个状态对并递增索引。你可以在 React Hooks:不是魔法,只是数组。 中了解更多关于这一机制的内容。
这个例子没有使用 React,但它能让你对 useState 的内部工作方式有一个概念:
let componentHooks = []; let currentHookIndex = 0; // useState 在 React 内部是如何工作的(简化版)。 function useState(initialState) { let pair = componentHooks[currentHookIndex]; if (pair) { // 这不是第一次渲染, // 所以状态对已经存在。 // 返回它并为下一次 Hook 调用做准备。 currentHookIndex++; return pair; } // 这是我们第一次渲染, // 所以创建一个状态对并存储它。 pair = [initialState, setState]; function setState(nextState) { // 当用户请求更改状态时, // 将新值放入该状态对中。 pair[0] = nextState; updateDOM(); } // 为将来的渲染保存这个状态对 // 并为下一次 Hook 调用做准备。 componentHooks[currentHookIndex] = pair; currentHookIndex++; return pair; } function Gallery() { // 每次 useState() 调用都会获得下一个状态对。 const [index, setIndex] = useState(0); const [showMore, setShowMore] = useState(false); function handleNextClick() { setIndex(index + 1); } function handleMoreClick() { setShowMore(!showMore); } let sculpture = sculptureList[index]; // 这个例子没有使用 React,所以 // 返回一个输出对象而不是 JSX。 return { onNextClick: handleNextClick, onMoreClick: handleMoreClick, header: `${sculpture.name} by ${sculpture.artist}`, counter: `${index + 1} of ${sculptureList.length}`, more: `${showMore ? 'Hide' : 'Show'} details`, description: showMore ? sculpture.description : null, imageSrc: sculpture.url, imageAlt: sculpture.alt }; } function updateDOM() { // 在渲染组件之前, // 重置当前 Hook 索引。 currentHookIndex = 0; let output = Gallery(); // 更新 DOM 以匹配输出。 // 这是 React 为你完成的部分。 nextButton.onclick = output.onNextClick; header.textContent = output.header; moreButton.onclick = output.onMoreClick; moreButton.textContent = output.more; image.src = output.imageSrc; image.alt = output.imageAlt; if (output.description !== null) { description.textContent = output.description; description.style.display = ''; } else { description.style.display = 'none'; } } let nextButton = document.getElementById('nextButton'); let header = document.getElementById('header'); let moreButton = document.getElementById('moreButton'); let description = document.getElementById('description'); let image = document.getElementById('image'); let sculptureList = [{ name: 'Homenaje a la Neurocirugía', artist: 'Marta Colvin Andrade', description: '虽然 Colvin 主要以暗指前西班牙时期符号的抽象主题而闻名,但这座巨大的雕塑——对神经外科的致敬——是她最具辨识度的公共艺术作品之一。', url: 'https://react.dev/images/docs/scientists/Mx7dA2Y.jpg', alt: '一尊青铜雕像,两个交叉的手指尖轻柔地托着一颗人脑。' }, { name: 'Floralis Genérica', artist: 'Eduardo Catalano', description: '这朵巨大的(75 英尺,约 23 米)银色花朵位于布宜诺斯艾利斯。它被设计成会移动,傍晚或强风吹拂时会闭合花瓣,清晨则会重新开放。', url: 'https://react.dev/images/docs/scientists/ZF6s192m.jpg', alt: '一座巨大的金属花朵雕塑,带有反光、镜面般的花瓣和粗壮的花蕊。' }, { name: 'Eternal Presence', artist: 'John Woodrow Wilson', description: 'Wilson 以其对平等、社会正义以及人类本质和精神特质的关注而闻名。这件巨大的青铜雕塑(7 英尺,约 2.13 米)代表了他所描述的“被赋予普遍人性意识的象征性黑人存在”。', url: 'https://react.dev/images/docs/scientists/aTtVpES.jpg', alt: '这座描绘人头的雕塑似乎无处不在且庄严肃穆,散发着平静与安宁。' }, { name: 'Moai', artist: 'Unknown Artist', description: '位于复活节岛的摩艾石像共有 1,000 尊,或者说是现存的纪念性雕像,由早期的拉帕努伊人创造,一些人认为它们代表着神化的祖先。', url: 'https://react.dev/images/docs/scientists/RCwLEoQm.jpg', alt: '三尊纪念性的石质半身像,头部异常巨大,表情忧郁。' }, { name: 'Blue Nana', artist: 'Niki de Saint Phalle', description: 'Nana 是欢欣鼓舞的生灵,象征着女性气质与母性。起初,Saint Phalle 为 Nana 使用织物和现成物件,后来又引入聚酯材料,以获得更鲜艳的效果。', url: 'https://react.dev/images/docs/scientists/Sd1AgUOm.jpg', alt: '一座大型马赛克雕塑,描绘了一个穿着彩色服装、跳舞的奇幻女性形象,洋溢着欢乐。' }, { name: 'Ultimate Form', artist: 'Barbara Hepworth', description: '这座抽象青铜雕塑是位于约克郡雕塑公园的《人类大家庭》系列的一部分。Hepworth 选择不去创作世界的写实再现,而是发展出受人物与风景启发的抽象形式。', url: 'https://react.dev/images/docs/scientists/2heNQDcm.jpg', alt: '一座由三个元素堆叠而成的高耸雕塑,让人联想到人体轮廓。' }, { name: 'Cavaliere', artist: 'Lamidi Olonade Fakeye', description: 'Fakeye 的作品传承自四代木雕匠人,融合了传统与当代约鲁巴主题。', url: 'https://react.dev/images/docs/scientists/wIdGuZwm.png', alt: '一座精致的木雕,描绘了一位面容专注的骑马战士,身上装饰着纹样。' }, { name: 'Big Bellies', artist: 'Alina Szapocznikow', description: 'Szapocznikow 以其碎片化身体雕塑而闻名,这些作品将身体作为青春与美丽脆弱和短暂的隐喻。这座雕塑描绘了两个非常逼真的大肚子,一个叠在另一个上面,每个大约五英尺(1.5 米)高。', url: 'https://react.dev/images/docs/scientists/AlHTAdDm.jpg', alt: '这座雕塑让人联想到一层层褶皱,与古典雕塑中的腹部形象大不相同。' }, { name: 'Terracotta Army', artist: 'Unknown Artist', description: '兵马俑是描绘秦始皇军队的陶俑雕塑集合。这支军队由 8,000 多名士兵、130 辆战车和 520 匹马,以及 150 匹骑兵马组成。', url: 'https://react.dev/images/docs/scientists/HMFmH6m.jpg', alt: '12 尊陶俑战士雕塑,每一尊都有独特的面部表情和盔甲。' }, { name: 'Lunar Landscape', artist: 'Louise Nevelson', description: 'Nevelson 以从纽约市废弃物中搜集物件而闻名,随后她会将它们组装成纪念性构筑物。在这件作品中,她使用了床柱、杂耍棒和座椅碎片等互不相同的部件,将它们钉上并粘进盒子里,体现了立体主义对空间与形式几何抽象的影响。', url: 'https://react.dev/images/docs/scientists/rN7hY6om.jpg', alt: '一座哑光黑色雕塑,单个元素起初难以分辨。' }, { name: 'Aureole', artist: 'Ranjani Shettar', description: 'Shettar 融合了传统与现代、自然与工业。她的艺术聚焦于人与自然之间的关系。她的作品被形容为在抽象和具象层面都引人入胜、违背重力,以及“对不太可能材料的精妙综合”。', url: 'https://react.dev/images/docs/scientists/okTpbHhm.jpg', alt: '一座浅色、类似金属丝的雕塑安装在混凝土墙上并向地面垂下,看起来很轻盈。' }, { name: 'Hippos', artist: 'Taipei Zoo', description: '台北动物园委托创作了一个河马广场,展示了半浸没在水中的河马嬉戏场景。', url: 'https://react.dev/images/docs/scientists/6o5Vuyu.jpg', alt: '一组青铜河马雕塑从人行道的缝隙中冒出,仿佛它们正在游泳。' }]; // 使 UI 与初始状态保持一致。 updateDOM();
你不必理解它也能使用 React,但你可能会发现这是一种有帮助的心智模型。
状态是隔离且私有的
状态只属于屏幕上某个组件实例。换句话说,如果你把同一个组件渲染两次,那么每个副本都会拥有完全隔离的状态! 修改其中一个不会影响另一个。
在这个示例中,前面提到的 Gallery 组件被渲染了两次,但其逻辑没有任何变化。试着点击每个画廊里的按钮。注意它们的状态是彼此独立的:
import Gallery from './Gallery.js'; export default function Page() { return ( <div className="Page"> <Gallery /> <Gallery /> </div> ); }
这就是为什么状态不同于你可能在模块顶部声明的普通变量。状态并不绑定到某个特定函数调用或代码中的某个位置,而是“局部”于屏幕上的特定位置。你渲染了两个 <Gallery /> 组件,所以它们的状态是分别存储的。
还要注意,Page 组件并不知道任何关于 Gallery 状态的事情,甚至不知道它是否有状态。不同于 props,状态对声明它的组件来说是完全私有的。 父组件不能修改它。这让你可以给任意组件添加状态,或者移除状态,而不会影响其他组件。
如果你希望两个画廊保持同步,该怎么做?在 React 中正确的做法是从子组件中移除状态,并将其提升到最近的共享父组件中。接下来的几页将聚焦于如何组织单个组件的状态,但我们会在组件间共享状态。中回到这个话题
Recap
- 当组件需要在多次渲染之间“记住”某些信息时,使用状态变量。
- 状态变量通过调用
useStateHook 来声明。 - Hook 是以
use开头的特殊函数。它们让你能够“接入” React 的特性,比如状态。 - Hook 可能会让你联想到导入:它们必须无条件地调用。调用 Hook,包括
useState,只在组件或另一个 Hook 的顶层有效。 useStateHook 返回一对值:当前状态和更新它的函数。- 你可以有多个状态变量。React 在内部根据它们的顺序来匹配它们。
- 状态对组件是私有的。如果你在两个地方渲染它,每个副本都会获得自己的状态。
Challenge 1 of 4: 完成画廊
当你按下最后一件雕塑上的“下一张”时,代码会崩溃。修复逻辑以防止崩溃。你可以通过在事件处理器中添加额外逻辑来实现,或者在动作不可用时禁用按钮。
修复崩溃后,再添加一个“上一张”按钮来显示上一件雕塑。它不应该在第一件雕塑上崩溃。
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}> 下一张 </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} /> </> ); }