状态:组件的记忆

组件通常需要根据交互来改变屏幕上显示的内容。向表单中输入内容应该更新输入字段,在图片轮播中点击“下一张”应该切换显示的图片,点击“购买”应该把商品加入购物车。组件需要“记住”一些东西:当前的输入值、当前的图片、购物车。在 React 中,这种组件特有的记忆被称为 state

You will learn

  • 如何使用 useState Hook 添加状态变量
  • useState Hook 返回的是哪一对值
  • 如何添加一个以上的状态变量
  • 为什么 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。但有两个原因导致这个变化不会显示出来:

  1. 局部变量不会在渲染之间保留。 当 React 第二次渲染这个组件时,它会从头开始渲染——不会考虑任何对局部变量所做的更改。
  2. 对局部变量的更改不会触发渲染。 React 不会意识到它需要使用新数据再次渲染该组件。

要用新数据更新组件,需要发生两件事:

  1. 在渲染之间保留 数据。
  2. 触发 React 使用新数据渲染组件(重新渲染)。

useState Hook 提供了这两件事:

  1. 一个用于在渲染之间保留数据的状态变量
  2. 一个用于更新该变量并触发 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。

Pitfall

Hook——以 use 开头的函数——只能在组件顶层或你自己的 Hook 顶层调用。 你不能在条件语句、循环或其他嵌套函数中调用 Hook。Hook 是函数,但把它们看作关于组件需求的无条件声明会更有帮助。你在组件顶部“使用” React 特性的方式,类似于你在文件顶部“导入”模块的方式。

useState 的结构

当你调用 useState 时,你是在告诉 React,你希望这个组件记住一些东西:

const [index, setIndex] = useState(0);

在这个例子中,你希望 React 记住 index

Note

惯例是把这一对命名为 const [something, setSomething]。你当然可以随意命名,但遵循惯例会让不同项目之间更容易理解。

useState 唯一的参数是你的状态变量的初始值。在这个例子中,index 的初始值通过 useState(0) 被设为 0

每次组件渲染时,useState 都会给你一个包含两个值的数组:

  1. 状态变量index),其值就是你存储的值。
  2. 状态设置函数setIndex),它可以更新状态变量并触发 React 再次渲染组件。

下面是它在实际中的工作方式:

const [index, setIndex] = useState(0);
  1. 你的组件第一次渲染。 因为你把 0 作为 index 的初始值传给了 useState,它会返回 [0, setIndex]。React 记住 0 是最新的状态值。
  2. 你更新状态。 当用户点击按钮时,会调用 setIndex(index + 1)index0,所以这就是 setIndex(1)。这告诉 React 现在要记住 index1,并触发另一次渲染。
  3. 组件的第二次渲染。 React 仍然看到的是 useState(0),但因为 React 记住 了你把 index 设为 1,所以它返回的是 [1, setIndex]
  4. 以此类推!

为组件设置多个状态变量

你可以在一个组件中拥有任意数量、任意类型的状态变量。这个组件有两个状态变量,一个数字 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}
      />
    </>
  );
}

如果某些状态彼此无关,比如这个例子中的 indexshowMore,那么拥有多个状态变量是个好主意。但如果你发现自己经常需要同时修改两个状态变量,那么把它们合并成一个可能会更容易。例如,如果你有一个包含许多字段的表单,那么拥有一个保存对象的单一状态变量,比为每个字段分别设置状态变量更方便。阅读 选择状态结构 以获取更多提示。

Deep Dive

React 如何知道要返回哪个状态?

你可能已经注意到,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

  • 当组件需要在多次渲染之间“记住”某些信息时,使用状态变量。
  • 状态变量通过调用 useState Hook 来声明。
  • Hook 是以 use 开头的特殊函数。它们让你能够“接入” React 的特性,比如状态。
  • Hook 可能会让你联想到导入:它们必须无条件地调用。调用 Hook,包括 useState,只在组件或另一个 Hook 的顶层有效。
  • useState Hook 返回一对值:当前状态和更新它的函数。
  • 你可以有多个状态变量。React 在内部根据它们的顺序来匹配它们。
  • 状态对组件是私有的。如果你在两个地方渲染它,每个副本都会获得自己的状态。

当你按下最后一件雕塑上的“下一张”时,代码会崩溃。修复逻辑以防止崩溃。你可以通过在事件处理器中添加额外逻辑来实现,或者在动作不可用时禁用按钮。

修复崩溃后,再添加一个“上一张”按钮来显示上一件雕塑。它不应该在第一件雕塑上崩溃。

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}
      />
    </>
  );
}