React State
1. State 基本概念
基本概念
State 是一个组件的私有内存。它用来“记住”那些会影响组件渲染输出并且会随用户交互而改变的信息,比如输入框里的文字、一个开关是开还是关、一个正在加载的数据等。
如果说
props
是从外部传递给组件的配置,那么state
就是组件内部自己维护的、随时间变化的数据。State 是让 React 应用变得动态和交互的核心。
State 和 props 的区别如下:
特性 | Props | State |
---|---|---|
数据来源 | 从父组件传递而来 | 在组件内部定义和管理 |
可变性 | 只读的,子组件绝不能修改 | 可变的,通过特定的 set 函数来更新 |
控制权 | 由父组件完全控制 | 由组件自身完全控制 |
作用 | 配置组件的外观和行为 | 记录组件的内部状态,响应交互 |
State 驱动 UI 更新
React 的一个核心原则是:当组件的 state 或 props 发生变化时,React 会自动重新渲染该组件及其子组件,以确保 UI 与数据保持同步。我们不需要手动操作 DOM,只需要声明式地更新 state,React 会负责解决剩下的一切。
2. useState
Hook
在函数式组件中,我们使用 useState
Hook 来为组件添加 state。
Hook 概念
Hooks 是 React 16.8 引入的特殊函数,它让我们可以在函数式组件中使用 state、生命周期等 React 特性。所有 Hooks 都以 use
开头,例如 useState
, useEffect
等。
useState
语法
useState
是最基础也是最常用的 Hook。它的使用方式如下:
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
// ...
}
useState
由如下组件组成:
- 初始值 (
initialValue
): 只在组件的第一次渲染时被使用。 - 状态变量 (
state variable
):count
,保存了当前的状态值。 - Setter 函数 (
setter function
):setCount
,用来触发 state 的更新。
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
};
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 变化与组件的重新渲染
React 渲染流程
React 的渲染主要包括下面三个步骤:
- 触发 (Trigger): 调用
set
函数会触发一次新的渲染请求。 - 渲染 (Render): React 重新调用你的组件函数,返回一个新的 UI “快照”(JSX)。
- 提交 (Commit): React 将新的 UI 快照与旧的进行比较(“Diffing”),然后只将变化的部分更新到真实 DOM 上。
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 结构能极大提升代码的可维护性。下面是一些常用的组织规范:
- 组合相关 state: 如果几个 state 总是同时更新,考虑将它们合并成一个对象。
- 避免冗余 state: 不要把可以从现有 props 或 state 计算出来的值放入 state。例如,有
firstName
和lastName
,就不需要一个fullName
的 state,直接在渲染时计算即可:const fullName = firstName + ' ' + lastName;
。 - 避免重复 state: 不要在多个 state 变量中存储相同的数据。这违反了“单一数据源”原则。
- 避免深度嵌套: 深度嵌套的 state 会让更新逻辑变得复杂。优先考虑扁平化的数据结构。
5. 在组件间共享 State
当多个子组件需要访问和操作同一个 state 时,应该将这个 state “提升”到它们最近的共同父组件中(Lifting State Up)。这被称为状态提升(这个在 props 相关笔记中也提到过)。具体过程如下:
- 在父组件中定义 state 和修改 state 的函数。
- 将 state 作为 prop 传递给需要读取它的子组件。
- 将修改 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)} />;
}
完整的流程如下:
- 用户交互:用户输入 h,
SearchInput
的onChange
事件被触发。 - 调用“报告”函数:
onChange
事件处理器执行onValueChange(e.target.value)
,此时e.target.value
是 h。而这个onValueChange
就是App
组件里的setSearchTerm
函数。所以,实际上是调用了setSearchTerm("h")
。 - 父组件状态更新:
App
组件的setSearchTerm
函数被执行,将searchTerm
的state
更新为 “h”。 - React 重新渲染:React 检测到
App
组件的state
发生了变化,于是重新渲染App
组件及其所有子组件。 - 新的 Props 向下流动:
- 在重新渲染过程中,
App
再次调用SearchInput
,但这次传给它的是value='h'
。输入框的值被更新。 App
再次调用DisplayResults
,但这次传给它的是term='h'
。DisplayResults
组件接收到新的 term,于是更新显示内容为"Searching for: h"
。
- 在重新渲染过程中,
6. 受控组件
定义
像 <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 规定了两条必须遵守的规则。
-
只能在顶层调用 Hook:不要在循环、条件判断或嵌套函数中调用 Hooks。必须确保在组件的每次渲染中,Hooks 的调用顺序都是完全相同的。
-
只能在 React 函数中调用 Hook:只能在 React 函数式组件或自定义 Hook 中调用 Hooks,不能在普通的 JavaScript 函数中调用。
React 内部依赖于 Hooks 在每次渲染时被调用的顺序来将 state 与正确的组件实例关联起来。如果调用顺序发生变化,这个对应关系就会错乱,导致严重的 bug。