React 状态管理

73 阅读10分钟

状态管理不是选库,是设计数据流。
useState 开始,到你真正需要外部库时才引入它。


技术介绍

React 组件的一切变化都源于状态。
状态管理要解决三件事:数据存在哪、谁来改、改了怎么通知视图

React 本身提供了 useStateuseReduceruseContext 三板斧。
当应用规模膨胀,出现“跨层级共享”“复杂更新逻辑”“性能瓶颈”时,才需要引入外部方案(如 Redux Toolkit、Zustand)。
掌握原则比背 API 重要:让状态尽可能局部,提升时别怕麻烦


必会基础

useState

函数组件的记忆单元。 让你在多次渲染之间保留一个值,并且值变化时触发重新渲染。

const [count, setCount] = useState(0);
// count:当前值  setCount:更新函数
// 基本用法:计数器
function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      点击了 {count} 次
    </button>
  );
}
// 函数式更新:避免闭包陷阱
function DelayedCounter() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setTimeout(() => {
      // 用函数形式拿到最新 state,而不是过期的闭包值
      setCount(prev => prev + 1);
    }, 1000);
  };

  return <button onClick={handleClick}>延迟+1:{count}</button>;
}

典型场景

  • 表单输入、开关、计数器等组件内部状态
  • 一切会触发 UI 变化的变量

边界提醒

  • 不要在循环或条件语句中调用 useState(会打乱 hooks 调用顺序)
  • 更新函数是异步的,紧挨着读取 count 可能还是旧值——需要新值用 prev

useReducer

当“下一个状态依赖前一个状态”,且逻辑复杂时,用 reducer 比多个 useState 更清晰。
它把状态更新收拢到一个纯函数里。

const [state, dispatch] = useReducer(reducer, initialState);
// 购物车状态管理
const cartReducer = (state, action) => {
  switch (action.type) {
    case 'ADD':
      return [...state, action.item];
    case 'REMOVE':
      return state.filter(item => item.id !== action.id);
    case 'CLEAR':
      return [];
    default:
      return state;
  }
};

function ShoppingCart() {
  const [cart, dispatch] = useReducer(cartReducer, []);

  return (
    <div>
      <button onClick={() => dispatch({ type: 'ADD', item: { id: 1, name: '书' } })}>
        添加
      </button>
      <button onClick={() => dispatch({ type: 'CLEAR' })}>
        清空
      </button>
      <ul>
        {cart.map(item => <li key={item.id}>{item.name}</li>)}
      </ul>
    </div>
  );
}

典型场景

  • 表单多字段联动校验
  • 购物车、多步骤向导等状态互相依赖的场景
  • 一个操作会同时修改多个状态值

边界提醒

  • 别把 useReducer 当成全局状态管理——它仍是组件私有
  • reducer 必须是纯函数:同样的 state 和 action 永远返回同样的新 state
  • 不要在里面做副作用(请求、定时器)

状态提升(Lifting State Up)

两个兄弟组件需要共享状态时,把状态移到它们最近的公共父组件。
这是 React 最基础的数据共享手段。

function Parent() {
  const [activeId, setActiveId] = useState(null);

  return (
    <div>
      <ItemList activeId={activeId} onSelect={setActiveId} />
      <ItemDetail id={activeId} />
    </div>
  );
}

典型场景

  • 选项卡切换,列表和详情联动
  • 受控输入组件的值需要被外部知道

边界提醒

  • 提升不是越高越好:升得太高会导致无意义的重渲染,且 props 传递路径过长
  • 一旦你觉得 props 开始“层层传递但中间组件不用”,就该考虑组合或 Context

核心原理

状态快照与闭包

每次渲染都有自己独立的 state 和 props(快照)。
事件处理函数会“记住”那次渲染时的值,这就是闭包陷阱的来源。

function AlertBug() {
  const [count, setCount] = useState(0);

  const showCount = () => {
    // 点击时创建的闭包捕获了当时的 count
    setTimeout(() => alert(count), 2000);
  };

  return (
    <div>
      <p>当前计数:{count}</p>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
      <button onClick={showCount}>2秒后弹出count</button>
    </div>
  );
}
// 快速点 +1 三次再点弹窗,弹出的仍是点击弹窗时的值

如何预判

  • 在异步操作里读取 state,一定问自己:这个值来自哪次渲染?
  • 用函数式更新或 useRef 来穿透闭包

渲染触发条件

状态变化(引用变化)触发重新渲染,父组件渲染也会导致子组件渲染。
理解这一点才能做性能优化。

  • useState 的 setter 使用 Object.is 比较,如果值相同则跳过
  • 父组件重渲染 → 子组件默认也跟着重渲染(即使 props 没变)

典型误判:以为改了对象内部属性就能触发渲染——你必须传新对象引用。

const [user, setUser] = useState({ name: 'Tom', age: 18 });

// ❌ 错误:直接修改后 setUser 同一个对象,不会重渲染
user.age = 19;
setUser(user);

// ✅ 正确:创建新对象
setUser({ ...user, age: 19 });

Redux 数据流与不可变性(原理衔接)

Redux 是基于 单向数据流 的状态管理库,它强制:

  1. 状态是单一不可变树(store)
  2. 改变状态的唯一方式是派发(dispatch)一个动作(action)
  3. 动作被纯函数(reducer)处理,返回全新的状态对象

这种“描述变化”而非“直接修改”的模式让状态变更可预测、可回溯,但也引入了样板代码。
Redux Toolkit 用 createSlice 内部集成 Immer,让你可以用“变异”写法,但 Immer 会在背后生成不可变更新。

关键心智:Redux 将组件状态的更新机制提取到组件外部统一管理,因此特别适合大型应用的多团队协作。


实战模式

受控表单通用处理(useState)

用一个 useState 对象承载表单,用 name 属性驱动统一更新。

function Form() {
  const [form, setForm] = useState({ username: '', email: '' });

  const handleChange = (e) => {
    const { name, value } = e.target;
    setForm(prev => ({ ...prev, [name]: value }));
  };

  return (
    <>
      <input name="username" value={form.username} onChange={handleChange} />
      <input name="email"    value={form.email}    onChange={handleChange} />
    </>
  );
}

为什么这么写:避免每个字段写一个 useState,方便后续校验和提交。


跨层级共享(Context + useReducer)

当数据和修改逻辑都需要跨组件传递时,用 Context 包裹 dispatch 和 state。

const CountContext = createContext();

function CountProvider({ children }) {
  const [count, dispatch] = useReducer(countReducer, 0);
  return (
    <CountContext.Provider value={{ count, dispatch }}>
      {children}
    </CountContext.Provider>
  );
}

// 任意子组件
function DeepChild() {
  const { count, dispatch } = useContext(CountContext);
  return <button onClick={() => dispatch({ type: 'inc' })}>{count}</button>;
}

典型场景:应用级的用户信息、主题、权限数据。

边界提醒

  • Context 值变化会重新渲染所有用到该 Context 的消费者(其下子树)
  • 不要把频繁变化的值(如每秒变化的时间)直接放 Context

Redux Toolkit(企业级状态管理)

Redux 的官方工具集,大幅简化 Redux 的配置和代码量。核心三要素:Store、Slice、组件连接

Redux Store 与 Slice (createSlice + configureStore)

一个 Slice 就是一个独立的状态片段及其更新逻辑,Store 是它们的集合。

import { configureStore, createSlice } from '@reduxjs/toolkit';

// 创建 Slice:自治的状态 + reducer + action 生成
const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0 },
  reducers: {
    increment: (state) => { state.value += 1; },  // 可“直接修改”,Immer 保证不可变
    decrement: (state) => { state.value -= 1; },
    incrementByAmount: (state, action) => {
      state.value += action.payload;
    },
  },
});

// 导出 action 创建函数
export const { increment, decrement, incrementByAmount } = counterSlice.actions;

// 配置 store,将 slice 的 reducer 挂载
const store = configureStore({
  reducer: {
    counter: counterSlice.reducer,
  },
});

典型场景

  • 需要全局共享的复杂状态,如用户信息、购物车、多页面表单数据
  • 开发大型项目需要状态变更的可视化(Redux DevTools 开箱即用)

边界提醒

  • 不要把所有状态都塞进 Redux:表单临时值、UI 开关等保留在组件本地更合适
  • Slice 的 reducer 函数里不能有副作用(异步请求等),需借助 createAsyncThunk 或中间件

组件连接 Redux (useSelector / useDispatch)

组件通过 hooks 读取状态和派发动作,不再需要 connect 高阶组件。

import { useSelector, useDispatch } from 'react-redux';
import { increment, incrementByAmount } from './counterSlice';

function Counter() {
  const count = useSelector((state) => state.counter.value); // 精确订阅需要的部分
  const dispatch = useDispatch();

  return (
    <div>
      <span>{count}</span>
      <button onClick={() => dispatch(increment())}>+</button>
      <button onClick={() => dispatch(incrementByAmount(5))}>+5</button>
    </div>
  );
}

典型场景

  • 任何需要访问或修改全局状态的组件
  • useSelector 的第二个参数可传入比较函数(如 shallowEqual)来避免不必要重渲染

边界提醒

  • useSelector 返回整个对象字面量时,每次 store 更新都会返回新引用,导致组件重渲染
  • 解决:分别取出原始值,或用 shallowEqual 比较

处理异步 (createAsyncThunk)

将异步逻辑从组件中剥离,像定义同步 action 一样定义异步操作。

import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';

// 定义异步 thunk
export const fetchUserById = createAsyncThunk(
  'user/fetchById',
  async (userId) => {
    const response = await fetch(`/api/users/${userId}`);
    return response.json();
  }
);

const userSlice = createSlice({
  name: 'user',
  initialState: { entity: null, status: 'idle' },
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchUserById.pending, (state) => { state.status = 'loading'; })
      .addCase(fetchUserById.fulfilled, (state, action) => {
        state.status = 'succeeded';
        state.entity = action.payload;
      })
      .addCase(fetchUserById.rejected, (state) => { state.status = 'failed'; });
  },
});

// 组件中直接 dispatch 这个 thunk,就像 dispatch 普通 action
dispatch(fetchUserById(123));

典型场景

  • 数据获取、提交表单等任何需要与后端交互的状态过程
  • 需要全局管理请求的 loading / error 状态

边界提醒

  • thunk 内部不要直接访问 store,通过 getState 参数可控读取
  • 简单的一次性请求(如登录表单)有时用组件内 useState + useEffect 更直接,不必上 Redux

轻量状态库(以 Zustand 为例)

当状态需要被多个无关组件共享,且追求极简配置时,引入库。

import { create } from 'zustand';

const useBearStore = create((set) => ({
  bears: 0,
  increase: () => set((state) => ({ bears: state.bears + 1 })),
  removeAll: () => set({ bears: 0 }),
}));

function Controls() {
  const increase = useBearStore((s) => s.increase);
  return <button onClick={increase}>加一头熊</button>;
}

选择时机:大量 props 穿梭、不相关的 UI 区域需要访问同一份数据,但不想要 Redux 的范式约束。


进阶与避坑

状态拆分 vs 合并

一个大的对象状态会导致无关组件重渲染,拆分成多个细粒度 state 是免费的性能优化。

// ❌ 一个对象,改 name 会导致 AgeDisplay 也重渲染
const [person, setPerson] = useState({ name: 'Tom', age: 20 });

// ✅ 拆开,改 name 只有 NameDisplay 重渲染
const [name, setName] = useState('Tom');
const [age, setAge] = useState(20);

权衡:如果这些值总是一起更新,合并反而更清晰,不用提前优化。


避免在渲染中设置状态

渲染期间调用 setState 会导致“太多重渲染”错误,并产生不可预测的结果。

// ❌ 致命错误
function Bad() {
  const [x, setX] = useState(0);
  if (x < 10) setX(x + 1);  // 渲染中更新状态
  return <div>{x}</div>;
}

唯一例外:通过 useEffect 或事件处理程序来更新,但记得正确设置依赖。


状态的不可变性(含 Redux Toolkit 的 Immer 陷阱)

React 和 Redux 都依赖引用比较判断更新。始终返回新对象、新数组,不要原地修改。

// 数组添加
setList(prev => [...prev, newItem]);  // 新数组
// 数组删除
setList(prev => prev.filter(item => item.id !== id));
// 数组修改某一项
setList(prev => prev.map(item => item.id === id ? {...item, done: true} : item));

Redux Toolkit 注意
createSlice 的 reducer 里你可以写 state.value = 10,这是 Immer 的魔法。
但如果脱离 createSlice(比如在 thunk 里手动更新),或把状态传给外部函数,必须保持不可变操作。
另外,console.log(state) 在 Immer 代理下可能看到 Proxy 对象,用 current(state) 打印原始内容。


Redux Selector 性能(避免无意义渲染)

useSelector 的默认比较是 ===,只要引用变化就重渲染。从 store 返回组合对象会每次创建新引用。

// ❌ 每次都返回新对象,组件一定重渲染
const { name, age } = useSelector(state => ({
  name: state.user.name,
  age: state.user.age,
}));

// ✅ 分别选取基本值(引用稳定)
const name = useSelector(state => state.user.name);
const age = useSelector(state => state.user.age);

// ✅ 或者使用 shallowEqual
import { shallowEqual } from 'react-redux';
const user = useSelector(state => state.user, shallowEqual);

进阶方案:用 createSelector 创建记忆化 selector,避免派生数据的重复计算。

import { createSelector } from '@reduxjs/toolkit';
const selectUser = state => state.user;
const selectName = createSelector(selectUser, user => user.name);
// 只有 user 变化时 selectName 才重新计算,引用也保持稳定

惰性初始化

当初始状态计算开销大时,传入一个函数,它只在首次渲染执行一次。

// ❌ 每次渲染都会调用 expensiveCalc(虽然只有首次的值被使用)
const [data] = useState(expensiveCalc());

// ✅ 惰性初始化
const [data] = useState(() => expensiveCalc());

明确场景:读取 localStorage、复杂数据转换等。


提防无意义的重渲染

通过将值稳定的对象提到组件外部,或使用 useMemouseRef 固定引用,减少子组件无效更新。

// 将这个常量定义在组件外面,因为它的引用永不变
const OPTIONS = [{ label: 'A', value: 1 }];

function Stable() {
  // 或者用 useRef 存不需要触发渲染的值
  const timerRef = useRef(null);
  // ...
}

实用心法:如果你传给子组件的 props 里包含内联对象/函数,子组件多半在受不必要的苦。


Redux 与 Context 的选择

场景推荐
少数组件间的共享状态状态提升或 Context
全局状态,更新频率低(如主题、用户信息)Context + useReducer
高频更新、复杂更新逻辑、需要中间件/调试工具Redux Toolkit
中小项目追求极简Zustand

常见错误:用 Redux 管理所有状态,连表单单次输入的值都放进 store,这会让应用变慢且难以维护。

状态管理的尽头,是合理的组件设计。
先把组件树和数据流理清楚,库只是最后的胶水。