「前端何去何从」(React教程)React 状态管理:从局部 State 到可扩展架构

11 阅读20分钟

React 状态管理:从局部 State 到可扩展应用

当 React 应用开始变复杂,真正决定代码质量的,往往不是组件写法,而是状态设计

状态管理是 React 真正开始变难的地方

在前两篇里,我们已经知道:

  • 组件会根据 props 和 state 渲染 UI
  • 用户操作会触发事件
  • 调用 setState 会让 React 重新渲染

但学到这里,很多人会开始遇到一个新的问题:代码明明能跑,为什么一复杂就开始乱?

真实项目里更常见的问题是:

  • 一个界面到底该定义哪些 state?
  • 多个组件需要同步时,状态放哪一层?
  • 为什么切换页面后,有的输入框内容还在,有的又被重置了?
  • 状态逻辑越来越复杂时,什么时候该用 useReducer
  • 跨很多层传数据时,什么时候该用 Context?

这些问题表面上看是在问 API,实际上问的是另一件事:

你的状态是不是设计清楚了。

React 官方文档把这一章叫做“状态管理”,我觉得非常准确。因为从这一章开始,重点已经不是“怎么写一个交互”,而是“怎么让交互在复杂度上升后仍然清晰、可维护”。

本文会覆盖什么

本文对应 React 官方文档“状态管理”章节及其子章节,按学习顺序讲解:

  • 用 State 响应输入
  • 选择 State 结构
  • 在组件间共享状态
  • 对 state 进行保留和重置
  • 迁移状态逻辑至 Reducer 中
  • 使用 Context 深层传递参数
  • 使用 Reducer 和 Context 拓展你的应用

学完之后你应该能做到什么

如果你认真跟着本文走完,应该能掌握这些能力:

  • 能把一个交互拆成几个明确的界面状态
  • 能判断哪些数据应该放进 state,哪些不该放
  • 能在兄弟组件之间正确共享状态
  • 能控制组件 state 什么时候保留、什么时候重置
  • 能把复杂的更新逻辑迁移到 reducer
  • 能用 context 避免层层传 props

这篇文章的目标不是让你记住几个 API 名字,而是让你建立一套更稳定的状态思维。


Part 1: 用 State 响应输入

不要一上来就写事件处理函数

React 官方文档在这一节强调的重点是:

先把界面看成一组状态,再去写组件。

很多初学者写交互时,习惯直接想:

  • 点击后把按钮禁用
  • 请求回来后显示成功提示
  • 出错时显示错误文案

这种写法的问题是,你是在“命令式地改界面”。界面稍微复杂一点,就会越来越乱。

React 更适合用另一种方式思考:

组件会处于哪些状态?每个状态应该显示什么 UI?哪些事件会让它切换到下一个状态?

这听起来有点抽象,但一旦你习惯了这种思路,很多交互代码会自然变简单。

第一步:列出组件的所有状态

以一个问答表单为例,你可以先列出这些状态:

  • 输入中
  • 提交中
  • 提交成功
  • 提交失败

这一步非常重要。很多 bug 的源头,不是代码写错,而是你一开始就漏掉了某个状态。

第二步:为状态选择合适的数据结构

来看一个典型实现:

import { useState } from 'react';

export default function QuizForm() {
  const [answer, setAnswer] = useState('');
  const [error, setError] = useState(null);
  const [status, setStatus] = useState('typing');

  async function handleSubmit(e) {
    e.preventDefault();
    setStatus('submitting');
    setError(null);

    try {
      await submitForm(answer);
      setStatus('success');
    } catch (err) {
      setError(err);
      setStatus('typing');
    }
  }

  if (status === 'success') {
    return <h1>答对了!</h1>;
  }

  return (
    <form onSubmit={handleSubmit}>
      <textarea
        value={answer}
        onChange={e => setAnswer(e.target.value)}
        disabled={status === 'submitting'}
      />
      <br />
      <button disabled={answer.length === 0 || status === 'submitting'}>
        提交
      </button>
      {error !== null && <p className="error">{error.message}</p>}
    </form>
  );
}

这里有三个 state:

  • answer:用户输入的答案
  • error:提交失败后的错误信息
  • status:当前界面所处状态

这三个值已经足够描述整个交互,而且没有多余信息。

为什么 status 比多个布尔值更好

很多人一开始会这样写:

const [isTyping, setIsTyping] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);

这样的问题是,很容易出现互相矛盾的状态,比如:

  • isSubmitting === true
  • isSuccess === true

这通常不是你想要的结果。

更好的方式是用一个 status 表示互斥状态:

const [status, setStatus] = useState('typing');

这样状态更清晰,也更不容易进入“不可能状态”。

第三步:找出触发状态变化的事件

状态变化通常来自两类来源:

  • 用户事件:输入、点击、提交
  • 异步结果:请求成功、请求失败

以上面那个表单为例:

  • onChange 会更新 answer
  • onSubmit 会把 status 改成 submitting
  • 请求成功后,把 status 改成 success
  • 请求失败后,更新 error,并把 status 改回 typing

这样一来,交互的流转路径就很清楚了。你不是在零散地改 UI,而是在管理状态切换。

这一节最应该记住什么

写交互时,先做这 3 件事:

  1. 列出所有可见状态
  2. 选出最少的 state 来表示它们
  3. 明确每个事件会把状态切换到哪里

如果你把这套顺序养成习惯,后面设计复杂界面会轻松很多。


Part 2: 选择 State 结构

State 结构设计得好,后面的代码会轻松很多;设计得差,后面每加一个功能都像在补漏洞。

React 官方文档在这一节给了几条非常重要的原则。这里我按教程方式,一条一条讲清楚。

如果你只记一句话,那就是:

state 应该尽可能少,但又足够表达当前界面。

原则 1:如果总是一起变化,就考虑合并

比如鼠标位置:

const [x, setX] = useState(0);
const [y, setY] = useState(0);

这能工作,但如果你每次更新时都要同时改 xy,更自然的写法是:

const [position, setPosition] = useState({ x: 0, y: 0 });

这样更适合表达“它们本来就是同一件事”。

不过也别为了“整洁”把所有状态都塞进一个对象。没有关系的 state,拆开反而更清楚。React 没有要求你一定用对象或一定拆开,关键看它们是不是同一个概念。

原则 2:避免矛盾的 state

看这个例子:

const [isSending, setIsSending] = useState(false);
const [isSent, setIsSent] = useState(false);

这两个 state 如果维护不好,就可能互相矛盾。

更好的写法通常是:

const [status, setStatus] = useState('typing');

用一个字段表达互斥状态,通常比多个布尔值更稳定。这是状态建模里非常常用的技巧。

原则 3:能计算出来的值,不要放进 state

这是最常见的错误之一。

// 不推荐
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState('');

这里的 fullName 完全可以由前两个值计算出来,不需要再单独存一份。

// 推荐
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');

const fullName = firstName + ' ' + lastName;

为什么不建议把 fullName 再存一份?

因为一旦存了两份数据,你就要保证它们一直同步。同步一旦漏掉,UI 就会出错。

这个问题在表单里尤其常见。我的建议是:能算出来的值,优先算,不要存。

原则 4:不要重复存同一份数据

假设你有一个商品列表,并且支持选中某一项:

const [items, setItems] = useState(initialItems);
const [selectedItem, setSelectedItem] = useState(items[0]);

这看起来很方便,但会产生同步问题。比如 items 更新了,selectedItem 可能还是旧对象。

更推荐的写法是只存 selectedId

const [items, setItems] = useState(initialItems);
const [selectedId, setSelectedId] = useState(0);

const selectedItem = items.find(item => item.id === selectedId);

这是一个很实用的经验,也是很多项目里常见的重构方向:

尽量存 ID,而不是存对象副本。

这样做的好处是,真正的数据源只有一份。后面列表更新、排序、过滤时,也不容易出现“选中的还是旧对象”这种问题。

原则 5:避免深度嵌套 state

如果 state 嵌套太深,更新会变得非常麻烦:

setPlan({
  ...plan,
  childPlaces: {
    ...plan.childPlaces,
    42: {
      ...plan.childPlaces[42],
      title: 'New title',
    },
  },
});

遇到这种情况时,优先考虑:

  • 扁平化数据结构
  • 用 ID 建立关系
  • 把局部状态拆到更小的组件里

一个简单的判断方法

写完一个 state 结构后,你可以问自己:

  • 这个值真的需要“记住”吗?
  • 它能不能从别的值算出来?
  • 它会不会和别的 state 打架?
  • 同一份数据是不是存了两次?

只要这里面有一两项答“是”,就说明 state 结构值得重审。

我在项目里看过很多“状态管理很乱”的组件,问题通常不在 React,而在这里一开始就没设计好。


Part 3: 在组件间共享状态

问题:兄弟组件各自有 state,但业务要求同步

这是 React 里非常经典的一类问题。

先看一个常见场景:手风琴组件。

如果每个 Panel 都自己维护是否展开:

function Panel({ title, children }) {
  const [isActive, setIsActive] = useState(false);

  return (
    <section>
      <h3>{title}</h3>
      {isActive ? (
        <p>{children}</p>
      ) : (
        <button onClick={() => setIsActive(true)}>显示</button>
      )}
    </section>
  );
}

这没问题,但它只能保证“每个面板自己能展开”。

如果需求变成:

同一时间只允许展开一个面板

那就不能让它们各自管理自己的 isActive 了。

解法:状态提升

React 的标准解法非常直接:把共享状态提升到最近的共同父组件。

import { useState } from 'react';

function Panel({ title, children, isActive, onShow }) {
  return (
    <section>
      <h3>{title}</h3>
      {isActive ? (
        <p>{children}</p>
      ) : (
        <button onClick={onShow}>显示</button>
      )}
    </section>
  );
}

export default function Accordion() {
  const [activeIndex, setActiveIndex] = useState(0);

  return (
    <>
      <Panel
        title="关于"
        isActive={activeIndex === 0}
        onShow={() => setActiveIndex(0)}
      >
        React 是一个用于构建用户界面的库。
      </Panel>
      <Panel
        title="词源"
        isActive={activeIndex === 1}
        onShow={() => setActiveIndex(1)}
      >
        “React” 这个名字强调界面对状态变化的响应。
      </Panel>
    </>
  );
}

这里发生了三件事:

  • Panel 自己不再保存 isActive
  • 父组件保存共享状态 activeIndex
  • 父组件通过 props 把值和事件处理函数传给子组件

这就是 React 文档里反复强调的单向数据流。状态放在父组件,值往下传,事件往上传。

什么时候该提升状态

当你发现两个或多个组件:

  • 需要显示同一份数据
  • 需要对同一件事达成一致
  • 一个组件更新后,另一个组件也要跟着变

那就应该考虑把状态提到它们最近的共同父级。

这个“最近”很重要。放得太低,没法共享;放得太高,顶层组件会被很多无关状态塞满。

所以“状态提升”不是把所有状态都往上丢,而是找到一个刚好能覆盖使用范围的父组件。

受控组件和非受控组件

在上面的例子里,Panel 的显示与否完全由父组件控制,这种组件叫受控组件

如果一个组件把状态完全保存在自己内部,父组件无法控制它,它就更接近非受控组件

这两个概念你不用死记定义,记住下面这个判断就够了:

  • 状态由父组件传入并控制:更偏受控
  • 状态封装在组件内部:更偏非受控

实际开发里,只要状态需要多个组件协作,通常就要往受控方向走。


Part 4: 对 State 进行保留和重置

这一节是 React 状态管理里最容易让人困惑的一节,但也非常重要。

React 什么时候会保留 state

React 会根据组件在渲染树中的位置来判断要不要保留 state。

你可以先记住一个最实用的结论:

同一种组件,出现在同一个位置,React 通常会保留它的 state。

看这个例子:

function App() {
  const [isFancy, setIsFancy] = useState(false);

  return (
    <div>
      {isFancy ? <Counter isFancy={true} /> : <Counter isFancy={false} />}
      <label>
        <input
          type="checkbox"
          checked={isFancy}
          onChange={e => setIsFancy(e.target.checked)}
        />
        使用 fancy 样式
      </label>
    </div>
  );
}

虽然 JSX 写了两个分支,但对 React 来说,这个位置渲染的始终都是 Counter。所以它会保留 Counter 的 state。

这也是为什么很多人会觉得 React 的 state “有点反直觉”。你看到的是两段 JSX,React 看到的是同一个位置上的同一种组件。

理解这点之后,很多“为什么这里没清空”“为什么这里还记着上一次输入”的问题,都会一下子变得很好解释。

React 什么时候会重置 state

如果某个位置上渲染的不再是同一种组件,React 就会重置 state。

例如:

{isPaused ? <p>See you later!</p> : <Counter />}

这里在同一个位置上,一会儿是 p,一会儿是 Counter。类型变了,所以 state 不会保留。

React 官方文档在这里讲得很清楚:state 不是“挂在 JSX 标签上”的,而是 React 根据渲染树位置保存的。位置和类型都变了,之前那份 state 自然就没了。

key 不只给列表用

很多人只知道列表渲染时要写 key,但 key 还有一个非常重要的用途:

告诉 React,这是两个不同身份的组件。

比如聊天窗口:

<Chat key={to.id} contact={to} />

如果你不给 key,切换聊天对象时,输入框里的内容可能会被保留下来。

如果你加上 key={to.id},React 会把不同联系人对应的 Chat 当成不同组件实例处理,输入框 state 就会重置。

所以,key 的本质不是“列表专用语法”,而是“身份标识”。

这点在表单、聊天窗口、切换用户视图这类场景里特别有用。

一个容易踩坑的错误:在组件内部定义组件

看下面的代码:

export default function App() {
  function MyTextField() {
    const [text, setText] = useState('');
    return <input value={text} onChange={e => setText(e.target.value)} />;
  }

  return <MyTextField />;
}

这种写法会让组件在每次渲染时都重新定义,React 可能把它当成一个新的组件,导致 state 被重置。

正确做法是把组件定义放在顶层。

这个问题前两篇其实也提到过。组件定义放在顶层,不只是为了代码风格,更是为了让 React 能稳定识别组件身份。

这一节最重要的结论

你可以把这节压缩成这 3 句:

  • state 跟组件在树中的位置有关
  • 类型相同、位置相同,state 通常会保留
  • 想强制重置,就改变组件身份,比如使用不同的 key

Part 5: 迁移状态逻辑至 Reducer 中

什么时候应该考虑 useReducer

一个组件只有一两个简单 state 时,useState 很好用。

但如果你开始遇到这些情况,就该考虑 reducer:

  • 同一个 state 会被很多事件处理函数修改
  • 更新逻辑越来越长
  • 组件里有很多 setXxx(...)
  • 你越来越难看清“某个操作到底改了什么”

这时候的问题通常不是“React 不够用”,而是“更新逻辑已经散了”。

我的经验是,只要你开始频繁在不同函数里改同一份 state,就该警觉了。因为这往往意味着后面会越来越难维护。

先看 useState 写法

以任务列表为例:

const [tasks, setTasks] = useState(initialTasks);

function handleAddTask(text) {
  setTasks([
    ...tasks,
    { id: nextId++, text, done: false },
  ]);
}

function handleChangeTask(task) {
  setTasks(tasks.map(t => (
    t.id === task.id ? task : t
  )));
}

function handleDeleteTask(taskId) {
  setTasks(tasks.filter(t => t.id !== taskId));
}

这没有错,但随着操作变多,逻辑会越来越分散。

把“怎么更新”收拢到 reducer

使用 reducer 后,事件处理函数只负责派发 action,也就是描述“发生了什么”:

function handleAddTask(text) {
  dispatch({
    type: 'added',
    id: nextId++,
    text,
  });
}

function handleChangeTask(task) {
  dispatch({
    type: 'changed',
    task,
  });
}

function handleDeleteTask(taskId) {
  dispatch({
    type: 'deleted',
    id: taskId,
  });
}

真正的更新逻辑集中写在 reducer:

function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added':
      return [
        ...tasks,
        {
          id: action.id,
          text: action.text,
          done: false,
        },
      ];
    case 'changed':
      return tasks.map(task =>
        task.id === action.task.id ? action.task : task
      );
    case 'deleted':
      return tasks.filter(task => task.id !== action.id);
    default:
      throw Error('Unknown action: ' + action.type);
  }
}

组件中这样使用:

import { useReducer } from 'react';

const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

React 官方文档把这个迁移过程拆成了三个步骤:

  1. 把设置 state 的逻辑改写成 dispatch action
  2. 编写 reducer 函数
  3. 在组件中用 useReducer

这个拆分很实用,因为它告诉你 reducer 不是“推倒重来”,而是一步一步迁移过去。

Reducer 的好处是什么

用了 reducer 之后:

  • 事件处理函数变短了
  • 更新逻辑集中到一个地方
  • 每个 action 都在描述“发生了什么”
  • reducer 是纯函数,更容易测试

我觉得 reducer 最大的价值,不是“更高级”,而是把状态更新逻辑从组件里剥出来。组件负责交互,reducer 负责状态变化,这样职责会清楚很多。

写 reducer 时的两个要求

1. reducer 必须是纯函数

不要在 reducer 里:

  • 发请求
  • 改外部变量
  • 直接修改原对象或原数组

它只应该根据 state + action 返回新的 state。

2. action 要表达业务动作

好的 action 例子:

dispatch({ type: 'deleted', id: 3 });
dispatch({ type: 'changed', task });

它们表达的是“用户做了什么”,而不是“我要调用哪个 setter”。

useStateuseReducer 怎么选

可以这样简单判断:

  • 简单组件:优先 useState
  • 更新逻辑复杂、操作很多:考虑 useReducer

不要为了“高级”而用 reducer。它的价值在于整理复杂逻辑,不在于替代所有 useState


Part 6: 使用 Context 深层传递参数

Context 解决什么问题

假设你有这样一棵组件树:

<Page>
  <Layout>
    <Sidebar />
    <Content>
      <Profile />
    </Content>
  </Layout>
</Page>

如果 Profile 需要当前登录用户,而这个用户数据在 Page 里,你可能会这样传:

<Layout user={user} />
<Content user={user} />
<Profile user={user} />

这就叫 prop drilling,也就是逐层透传 props。

如果中间组件根本不关心这个数据,只是被迫传下去,代码会越来越烦。

Context 就是为了解决这个问题。

你可以把它理解成一种“跨中间层传值”的机制。

React 官方文档这里有一句话我很认同:如果数据可以“不经过 props 直达需要它的组件”,很多中间层组件就能干净很多。

Context 的基本三步

第一步:创建 context
import { createContext } from 'react';

export const LevelContext = createContext(1);
第二步:提供 context
import { LevelContext } from './LevelContext.js';

function Section({ level, children }) {
  return (
    <LevelContext value={level}>
      {children}
    </LevelContext>
  );
}
第三步:读取 context
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';

function Heading({ children }) {
  const level = useContext(LevelContext);

  switch (level) {
    case 1:
      return <h1>{children}</h1>;
    case 2:
      return <h2>{children}</h2>;
    case 3:
      return <h3>{children}</h3>;
    default:
      return <p>{children}</p>;
  }
}

这样,Heading 不需要一级一级接收 level,它会直接从最近的 provider 读取。

这个例子看起来简单,但它很好地说明了 Context 最适合做什么:提供一个“周围环境”。像标题层级、主题、语言、当前用户,都属于这类信息。

Context 的特点

你需要记住两点:

  • useContext(SomeContext) 会读取最近的 provider 提供的值
  • 如果上层没有 provider,就使用 createContext 时传入的默认值

什么情况下该用 Context

比较适合 Context 的数据:

  • 主题
  • 当前用户
  • 语言环境
  • 路由信息
  • 某个功能域内多层组件都要读取的状态

什么情况下不要急着用 Context

如果只是传一两层,props 往往更直接。

因为 props 的依赖关系是显式的,读代码时很容易看懂。而 Context 一旦用多了,数据来源会变得不够直观。

所以一个很实用的建议是:

先问自己,这个值是真的“很多层都要用”吗?如果不是,就先用 props。

React 官方文档还提醒了另一种替代思路:有时候你不需要传某个具体 prop,而是可以把 JSX 作为 children 往下传。这样也能减少中间层的负担。


Part 7: 使用 Reducer 和 Context 拓展你的应用

到了这里,你已经学了两件事:

  • useReducer 可以整理复杂的状态更新逻辑
  • Context 可以避免层层传递 props

把它们结合起来,就是 React 原生组织复杂状态的一种常见方式。

如果说 reducer 解决的是“逻辑分散”,那 Context 解决的就是“传递太深”。

一个典型问题

任务列表状态在顶层:

const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

接下来你会发现:

  • TaskList 需要读 tasks
  • AddTask 需要用 dispatch
  • Task 也需要用 dispatch

如果继续用 props 层层往下传,组件树很快会变得臃肿。

官方文档的常见做法

statedispatch 放进两个不同的 context。

import { createContext } from 'react';

export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);

然后统一提供:

import { useReducer } from 'react';

function TaskApp() {
  const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

  return (
    <TasksContext value={tasks}>
      <TasksDispatchContext value={dispatch}>
        <h1>任务列表</h1>
        <AddTask />
        <TaskList />
      </TasksDispatchContext>
    </TasksContext>
  );
}

深层组件直接读取:

import { useContext } from 'react';

function TaskList() {
  const tasks = useContext(TasksContext);

  return (
    <ul>
      {tasks.map(task => (
        <li key={task.id}>{task.text}</li>
      ))}
    </ul>
  );
}
function AddTask() {
  const dispatch = useContext(TasksDispatchContext);

  function handleAdd(text) {
    dispatch({
      type: 'added',
      id: nextId++,
      text,
    });
  }

  // ...
}

为什么分成两个 context

这是一个很常见的组织方式,因为它把:

  • 读取状态
  • 触发更新

分开了。

这样做的一个好处是,读代码时你会更容易分清:哪些组件只是消费数据,哪些组件还会发起更新。

React 官方文档在后面还进一步把这部分封装成 useTasksuseTasksDispatch。这个思路很好,因为它能把“从哪里取数据”也一起隐藏起来,让业务组件更干净。

后面如果你想封装自定义 Hook,也会更清楚:

function useTasks() {
  return useContext(TasksContext);
}

function useTasksDispatch() {
  return useContext(TasksDispatchContext);
}

什么时候适合这样做

Reducer + Context 比较适合这些场景:

  • 某个功能模块已经有一定复杂度
  • 多层组件都要读写同一份状态
  • 你还不想引入额外状态管理库

它不是所有项目都必须上,但它是 React 原生能力里非常值得掌握的一种模式。

如果项目规模还小,直接 props 或局部 state 完全没问题。只有当一个功能域已经明显变复杂时,Reducer + Context 才会真正体现价值。


学习路径与实践建议

到这里,你可以把本章内容串成一条完整的学习链路:

  1. 先学会把 UI 拆成几个状态
  2. 再学会只保存必要的 state
  3. 接着学会把共享状态提升到父组件
  4. 再理解 React 为什么会保留或重置 state
  5. 状态逻辑复杂后,用 reducer 收拢更新
  6. 组件层级太深后,用 context 传递状态或 dispatch

最常见的几个错误

如果你刚开始练习状态管理,最容易犯这些错:

  • 把能计算出来的值也放进 state
  • 用多个布尔值描述互斥状态
  • 兄弟组件各自维护本该共享的数据
  • 不理解 key,导致 state 该重置时没重置
  • 过早使用 Context,让数据流变复杂

你可以把这几个问题当成自己的排错清单。

如果你发现某个组件越来越难懂,通常可以反过来检查:

  • state 是不是存多了
  • 谁拥有这份 state 是不是不清楚
  • 更新逻辑是不是散在太多地方
  • 组件身份是不是不稳定

很多所谓“React 状态管理问题”,最后都能落回这几个基本点。


总结

这一章最核心的不是 API,而是状态思维。

  • 用 State 响应输入:先定义状态,再写 UI
  • 选择 State 结构:只保存必要且不冲突的数据
  • 共享状态:把共享数据放到最近的共同父级
  • 保留和重置:理解位置、类型和 key
  • Reducer:把复杂的更新逻辑集中起来
  • Context:减少层层传递 props 的成本
  • Reducer + Context:在 React 原生能力内组织更复杂的应用

如果你能把这一章真正吃透,后面再学表单、路由、全局状态管理库,都会顺很多。

如果说前两篇解决的是“React 是怎么渲染 UI 和响应交互的”,那这一篇解决的就是另一件更实际的事:

当你的应用不再只是几个按钮和输入框时,状态应该怎么组织,代码才不会失控。


相关资源


本文基于 React 官方文档 “状态管理” 章节。