React 组件状态 (State)

在前面的章节中,我们学习了 props,它允许我们将数据从父组件传递到子组件。然而,props 是只读的。组件不能改变自己的 props

那么,当一个组件需要根据用户交互、网络响应或其他事件来改变自己的数据时,该怎么办呢?答案就是 State (状态)

State 与 props 类似,但它是私有的,并且完全由组件自己控制。

引入 useState Hook

在函数组件中,我们使用 useState Hook 来为组件添加 state。

什么是 Hook? Hook 是 React 16.8 版本引入的新特性。它们是一些特殊的函数,可以让你在函数组件中使用 state 以及其他 React 特性。我们将在后面的章节中深入学习 Hooks。

让我们来看一个例子。我们将创建一个有时钟功能的组件,它会自己更新时间。

之前我们用 setIntervalroot.render() 来实现,但那不是组件化的思想。现在,我们让 Clock 组件自己管理自己的状态。

import React, { useState, useEffect } from 'react';

function Clock() {
  // 1. 使用 useState 定义一个名为 date 的 state 变量
  const [date, setDate] = useState(new Date());

  // 2. 使用 useEffect 来设置定时器
  useEffect(() => {
    const timerID = setInterval(() => {
      setDate(new Date()); // 3. 更新 state
    }, 1000);

    // 清除定时器
    return () => {
      clearInterval(timerID);
    };
  }, []); // 空数组表示这个 effect 只在组件挂载时运行一次

  return (
    <div>
      <h1>Hello, world!</h1>
      <h2>It is {date.toLocaleTimeString()}.</h2>
    </div>
  );
}

export default Clock;

代码解析

  1. const [date, setDate] = useState(new Date());

    • 我们调用 useState Hook,并传入初始状态 new Date()
    • useState 返回一个包含两个值的数组:
      1. 当前的状态值 (date)。
      2. 一个可以用来更新状态的函数 (setDate)。
    • 我们使用数组解构语法 [date, setDate] 来给这两个值命名。
  2. useEffect(...)

    • useEffect Hook 用于处理副作用(side effects),比如数据获取、订阅或者手动修改 DOM。
    • 在这里,我们用它来设置一个每秒钟都会执行的定时器 setInterval
    • useEffect 的第二个参数 [] 是一个依赖数组。当数组为空时,意味着这个 effect 只会在组件第一次渲染到屏幕上(挂载)后执行一次。
    • useEffect 返回的函数是一个清理函数。它会在组件从屏幕上移除(卸载)时执行,用于清除定时器,防止内存泄漏。
  3. setDate(new Date())

    • 在定时器的回调函数中,我们调用 setDate 并传入一个新的 Date 对象。
    • 这是更新 state 的唯一正确方式。
    • setDate 被调用时,React 会重新渲染 Clock 组件,并使用最新的 date 值。

State 更新是异步的

当你调用 setState 函数时,React 可能会将多个 setState 调用合并成一个单一的更新来提高性能。这意味着 this.propsthis.state 可能是异步更新的,你不应该依赖它们的值来计算下一个状态。

例如,下面的代码可能不会按预期工作:

// 错误示例
function Counter() {
  const [count, setCount] = useState(0);

  function handleIncrement() {
    setCount(count + 1); // setCount(0 + 1)
    setCount(count + 1); // 仍然是 setCount(0 + 1)
    setCount(count + 1); // 仍然是 setCount(0 + 1)
  }
  // ...
}

因为 count 的值在同一次渲染中不会改变,所以三次调用 setCount 的效果和一次是相同的。

正确的做法:使用函数式更新

如果你需要基于前一个 state 来计算新 state,你可以向 setState 函数传递一个函数。这个函数会接收前一个状态作为参数,并返回新的状态。

// 正确示例
function Counter() {
  const [count, setCount] = useState(0);

  function handleIncrement() {
    setCount(prevCount => prevCount + 1);
    setCount(prevCount => prevCount + 1);
    setCount(prevCount => prevCount + 1);
  }
  // ...
}

现在,每次调用 setCount 都会接收到上一次更新后的最新 state,最终 count 会增加 3。

总结

  • state 用于存储组件内部会随时间变化的数据。
  • 使用 useState Hook 在函数组件中添加和管理 state。
  • 调用 useState 返回当前 state 和一个更新 state 的函数 [value, setValue]
  • 通过调用 setValue 函数来更新 state,这会触发组件的重新渲染。
  • State 更新可能是异步的,当新 state 依赖于旧 state 时,应使用函数式更新。

接下来,我们将学习组件的另一个重要概念:生命周期。