添加交互性

屏幕上的某些内容会根据用户输入而更新。例如,点击图片图库会切换当前图片。在 React 中,随时间变化的数据称为 state。你可以向任何组件添加 state,并在需要时更新它。在本章中,你将学习如何编写能够处理交互、更新其 state 并随着时间显示不同输出的组件。

响应事件

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

Ready to learn this topic?

阅读 响应事件 以了解如何添加事件处理器。

Read More

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

Ready to learn this topic?

阅读 State: A Component’s Memory 以了解如何记住一个值并在交互时更新它。

Read More

渲染与提交

在你的组件显示到屏幕上之前,它们必须先由 React 渲染。理解这个过程的步骤将帮助你思考代码是如何执行的,以及解释它的行为。

想象你的组件是厨房里的厨师,用各种食材做出美味佳肴。在这个场景中,React 是服务员,它接收顾客的点单并把订单送到厨房。这个请求和提供 UI 的过程分为三步:

  1. 触发一次渲染(把食客的订单送到厨房)
  2. 渲染组件(在厨房里准备订单)
  3. 提交到 DOM(把订单端上桌)
  1. React 作为餐厅里的服务员,从用户那里取订单并将它们送到组件厨房。
    触发
  2. 卡片厨师给 React 一个新的 Card 组件。
    渲染
  3. React 将 Card 送到用户的餐桌上。
    提交

Illustrated by Rachel Lee Nabors

Ready to learn this topic?

阅读 渲染与提交,了解一次 UI 更新的生命周期。

Read More

状态如同快照

与普通的 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>
  );
}

Ready to learn this topic?

阅读 状态如同快照,了解为什么状态在事件处理器内部看起来是“固定的”且不会变化。

Read More

排队一系列状态更新

这个组件有 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

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

Ready to learn this topic?

阅读 排队一系列状态更新,了解如何对一系列状态更新进行排队。

Read More

更新状态中的对象

状态可以保存任何类型的 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": {}
}

Ready to learn this topic?

阅读 更新状态中的对象,了解如何正确地更新对象。

Read More

在 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": {}
}

Ready to learn this topic?

阅读 在 state 中更新数组 以了解如何正确更新数组。

Read More

接下来是什么?

前往 响应事件 开始逐页阅读本章!

或者,如果你已经熟悉这些主题,为什么不读一读 管理 State 呢?