Dark Dwarf Blog background

React State

React State

1. State 基本概念

a.a. 基本概念

State 是一个组件的私有内存。它用来“记住”那些会影响组件渲染输出并且会随用户交互而改变的信息,比如输入框里的文字、一个开关是开还是关、一个正在加载的数据等。

如果说 props 是从外部传递给组件的配置,那么 state 就是组件内部自己维护的、随时间变化的数据。State 是让 React 应用变得动态和交互的核心

State 和 props 的区别如下:

特性PropsState
数据来源从父组件传递而来在组件内部定义和管理
可变性只读的,子组件绝不能修改可变的,通过特定的 set 函数来更新
控制权由父组件完全控制由组件自身完全控制
作用配置组件的外观和行为记录组件的内部状态,响应交互

b.b. State 驱动 UI 更新

React 的一个核心原则是:当组件的 state 或 props 发生变化时,React 会自动重新渲染该组件及其子组件,以确保 UI 与数据保持同步。我们不需要手动操作 DOM,只需要声明式地更新 state,React 会负责解决剩下的一切。

2. useState Hook

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

a.a. Hook 概念

Hooks 是 React 16.8 引入的特殊函数,它让我们可以在函数式组件中使用 state、生命周期等 React 特性。所有 Hooks 都以 use 开头,例如 useState, useEffect 等。

b.b. useState 语法

useState 是最基础也是最常用的 Hook。它的使用方式如下:

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  // ...
}

useState 由如下组件组成:

  • 初始值 (initialValue): 只在组件的第一次渲染时被使用。
  • 状态变量 (state variable): count,保存了当前的状态值。
  • Setter 函数 (setter function): setCount,用来触发 state 的更新。

c.c. State 更新规则

调用 setCount(1) 并不会立即改变 count 变量的值。它只是告诉 React:“我请求在下一次渲染时将 count 的值更新为 1”。React 会将短时间内触发的多个 state 更新批量处理(batching),以优化性能,这意味着多次 set 调用通常只会引发一次重新渲染

因此,如果我们需要基于前一个 state 来计算新的 state,应该给 set 函数传递一个函数。这可以避免因 state 异步更新而导致的问题。

const handleClick = () => {
  // 错误的方式:基于当前渲染的旧 state 计算
  setCount(count + 1); // count 在这里是 0, 请求更新为 1
  setCount(count + 1); // count 在这里还是 0, 再次请求更新为 1
  // 结果:count 只会增加 1

  // 正确的方式:使用函数式更新
  setCount(prevCount => prevCount + 1); // 告诉 React: "取最新的 count 值,然后加 1"
  setCount(prevCount => prevCount + 1); // 告诉 React: "取最新的 count 值,然后加 1"
  // 结果:React 会将这两个更新排入队列,最终 count 会增加 2
};

e.e. State 的不可变性

对于对象和数组这类引用类型,绝不能直接修改它们,而是应该创建一个新的对象或数组来替换旧的。直接修改会导致 React 无法检测到变化,从而不重新渲染。

function Profile() {
  const [person, setPerson] = useState({ name: "John", age: 100 });

  const handleIncreaseAge = () => {
    // 错误!直接修改了 state 对象
    // person.age++;
    // setPerson(person);

    // 正确!创建一个新对象,并复制旧对象的内容
    const newPerson = { ...person, age: person.age + 1 };
    setPerson(newPerson);
  };
}

React 使用 Object.is() 来比较新旧 state。如果我们传递的是同一个对象的引用,React 会认为 state 没有变化

3. State 变化与组件的重新渲染

a.a. React 渲染流程

React 的渲染主要包括下面三个步骤:

  1. 触发 (Trigger): 调用 set 函数会触发一次新的渲染请求。
  2. 渲染 (Render): React 重新调用你的组件函数,返回一个新的 UI “快照”(JSX)。
  3. 提交 (Commit): React 将新的 UI 快照与旧的进行比较(“Diffing”),然后只将变化的部分更新到真实 DOM 上。

b.b. State 的快照属性

State 在一次特定的渲染中是固定的。在某次渲染的事件处理函数中,state 的值永远是那次渲染时的快照 (Snapshot) 值

function Person() {
  const [person, setPerson] = useState({ name: "John", age: 100 });

  const handleIncreaseAge = () => {
    // 这里的 person 是本次渲染时的快照,age 是 100
    console.log("in handleIncreaseAge (before setPerson call): ", person);
    setPerson({ ...person, age: person.age + 1 });
    // 调用 setPerson 后,这里的 person 仍然是 age 为 100 的旧快照
    console.log("in handleIncreaseAge (after setPerson call): ", person);
  };

  // 只有在下一次重新渲染时,person.age 才会变为 101
  console.log("during render: ", person);

  return <button onClick={handleIncreaseAge}>Increase age</button>;
}

4. State 组织原则

良好的 state 结构能极大提升代码的可维护性。下面是一些常用的组织规范:

  1. 组合相关 state: 如果几个 state 总是同时更新,考虑将它们合并成一个对象。
  2. 避免冗余 state: 不要把可以从现有 props 或 state 计算出来的值放入 state。例如,有 firstNamelastName,就不需要一个 fullName 的 state,直接在渲染时计算即可:const fullName = firstName + ' ' + lastName;
  3. 避免重复 state: 不要在多个 state 变量中存储相同的数据。这违反了“单一数据源”原则。
  4. 避免深度嵌套: 深度嵌套的 state 会让更新逻辑变得复杂。优先考虑扁平化的数据结构。

5. 在组件间共享 State

当多个子组件需要访问和操作同一个 state 时,应该将这个 state “提升”到它们最近的共同父组件中(Lifting State Up)。这被称为状态提升(这个在 props 相关笔记中也提到过)。具体过程如下:

  1. 在父组件中定义 state 和修改 state 的函数。
  2. 将 state 作为 prop 传递给需要读取它的子组件。
  3. 将修改 state 的函数作为 prop 传递给需要更新它的子组件。
function App() {
  const [searchTerm, setSearchTerm] = useState('');

  return (
    <>
      {/* 父组件拥有 state 和 handler */}
      <SearchInput value={searchTerm} onValueChange={setSearchTerm} />
      <DisplayResults term={searchTerm} />
    </>
  );
}

// SearchInput 是一个受控组件,它通知父组件更新
function SearchInput({ value, onValueChange }) {
  return <input value={value} onChange={e => onValueChange(e.target.value)} />;
}

完整的流程如下:

  1. 用户交互:用户输入 h,SearchInputonChange 事件被触发。
  2. 调用“报告”函数:onChange 事件处理器执行 onValueChange(e.target.value),此时 e.target.value 是 h。而这个 onValueChange 就是 App 组件里的 setSearchTerm 函数。所以,实际上是调用了 setSearchTerm("h")
  3. 父组件状态更新:App 组件的 setSearchTerm 函数被执行,将 searchTermstate 更新为 “h”。
  4. React 重新渲染:React 检测到 App 组件的 state 发生了变化,于是重新渲染 App 组件及其所有子组件。
  5. 新的 Props 向下流动:
    • 在重新渲染过程中,App 再次调用 SearchInput,但这次传给它的是 value='h'。输入框的值被更新。
    • App 再次调用 DisplayResults,但这次传给它的是 term='h'
    • DisplayResults 组件接收到新的 term,于是更新显示内容为 "Searching for: h"

6. 受控组件

a.a. 定义

<input>, <textarea>, <select> 这样的表单元素,其值默认由 DOM 自身管理。**受控组件 (Controlled Components)**是一种模式,其中 React 组件的 state 成为表单元素的“唯一数据源”。

我们通过将表单元素的 value 属性绑定到 React 的 state 变量,并使用 onChange 事件来更新这个 state 变量,从而实现对表单元素的完全控制。

function NameForm() {
  const [name, setName] = useState('');

  return (
    <input
      type="text"
      value={name} // React state 控制了 input 的值
      onChange={(e) => setName(e.target.value)} // 用户的输入通过 onChange 更新 React state
    />
  );
}

这个模式确保了应用的 state 和 UI 总是一致的。

7. Hook 的使用规则

为了让 Hooks 能够正常工作,React 规定了两条必须遵守的规则。

  1. 只能在顶层调用 Hook:不要在循环、条件判断或嵌套函数中调用 Hooks。必须确保在组件的每次渲染中,Hooks 的调用顺序都是完全相同的。

  2. 只能在 React 函数中调用 Hook:只能在 React 函数式组件或自定义 Hook 中调用 Hooks,不能在普通的 JavaScript 函数中调用。

React 内部依赖于 Hooks 在每次渲染时被调用的顺序来将 state 与正确的组件实例关联起来。如果调用顺序发生变化,这个对应关系就会错乱,导致严重的 bug。