第二章:React原生状态——从useState到Context API的深度解析

大家好,我是你们这门课的主讲。今天咱们来聊聊React最基础、也最容易踩坑的部分——原生状态管理。

说实话,我见过太多项目,明明用useState就能搞定的事,非要上Redux。也见过相反的情况——一个简单的Context被滥用,导致整个应用卡成PPT。嗯,今天我们就来把这些坑一个个填平。

2.1 useState:比你想象的更复杂

先说说useState。很多人觉得它简单,不就是存个状态嘛。但我在项目中遇到过好几次线上bug,追根溯源都是useState用错了。

2.1.1 状态更新的异步陷阱

看这段代码,你觉得会输出什么?

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

function handleClick() {
  setCount(count + 1);
  setCount(count + 1);
  console.log(count); // 输出多少?
}

答案是0。而且count最终只加了1,不是2。

为什么会这样?因为React的状态更新是异步的。同一个事件循环里的多次setCount会被合并。说白了,两次setCount拿到的count都是旧值0。

避坑指南: 如果你真的想连续更新两次,用函数式更新:
setCount(prev => prev + 1);
setCount(prev => prev + 1); // 这样就能加2了

2.1.2 对象状态的更新

我个人习惯,只要状态是对象,一定用useReducer或者immer。为什么?看这个:

const [user, setUser] = useState({ name: '张三', age: 25 });

// 错误写法
setUser({ name: '李四' }); // age丢了!

// 正确写法
setUser(prev => ({ ...prev, name: '李四' }));

你想想看,如果对象嵌套三层以上,每次都要手动展开,代码能看吗?我建议:对象超过两层,直接上useReducer。

2.2 useReducer:状态逻辑的救星

useReducer不是Redux的替代品,它是React自带的。说白了,它就是useState的升级版——当状态逻辑复杂时,用reducer把逻辑集中管理。

2.2.1 什么时候该用?

我有个简单的判断标准:

  • 状态有多个子值,且更新逻辑相互依赖 → 用useReducer
  • 状态更新需要依赖前一个状态 → 用useReducer
  • 状态逻辑可以复用 → 用useReducer

举个例子,一个表单状态:

const initialState = {
  name: '',
  email: '',
  age: 0,
  errors: {}
};

function formReducer(state, action) {
  switch (action.type) {
    case 'SET_FIELD':
      return {
        ...state,
        [action.field]: action.value,
        errors: { ...state.errors, [action.field]: null }
      };
    case 'SET_ERRORS':
      return { ...state, errors: action.errors };
    case 'RESET':
      return initialState;
    default:
      return state;
  }
}

// 使用
const [state, dispatch] = useReducer(formReducer, initialState);
dispatch({ type: 'SET_FIELD', field: 'name', value: '李四' });

你看,所有更新逻辑都集中在reducer里,组件只负责派发动作。维护起来舒服多了。

小技巧: 我习惯把reducer和action类型定义放在单独的文件里。这样测试也方便,直接测reducer纯函数就行。

2.3 Context API:性能陷阱与正确姿势

Context API是个好东西,但用不好就是性能杀手。我曾经在一个项目中,把用户信息放在顶层Context里,结果每次用户信息一更新,整个应用都重新渲染——那叫一个酸爽。

2.3.1 原理:为什么Context会导致重渲染?

Context的机制是这样的:当Context的值变化时,所有消费该Context的组件都会重新渲染。注意,是所有,不管你有没有用到变化的那部分数据。

举个例子:

const AppContext = React.createContext();

function App() {
  const [user, setUser] = useState({ name: '张三' });
  const [theme, setTheme] = useState('light');

  return (
    <AppContext.Provider value={{ user, theme, setUser, setTheme }}>
      <UserProfile />
      <ThemeToggle />
    </AppContext.Provider>
  );
}

function ThemeToggle() {
  const { theme, setTheme } = useContext(AppContext);
  // 这里只用了theme,但user变了它也会重渲染!
  return <button>{theme}</button>;
}

为什么会这样?因为每次setUser都会创建新的value对象,导致所有消费者都认为Context变了。

2.3.2 解决方案:拆分Context

我建议的做法是:把不相关的状态拆成多个Context。

const UserContext = React.createContext();
const ThemeContext = React.createContext();

function App() {
  const [user, setUser] = useState({ name: '张三' });
  const [theme, setTheme] = useState('light');

  return (
    <UserContext.Provider value={{ user, setUser }}>
      <ThemeContext.Provider value={{ theme, setTheme }}>
        <UserProfile />
        <ThemeToggle />
      </ThemeContext.Provider>
    </UserContext.Provider>
  );
}

// 现在ThemeToggle只订阅ThemeContext,user变了它不会重渲染
核心原则: Context的粒度要细。一个Context只放一组相关的状态。就像你家的电闸,每个房间一个开关,而不是全屋一个总闸。

2.3.3 另一个技巧:用useMemo缓存value

如果实在不想拆分,可以用useMemo:

const value = useMemo(() => ({ user, setUser }), [user]);

return (
  <UserContext.Provider value={value}>
    {children}
  </UserContext.Provider>
);

这样只有当user变化时,value引用才会变。但说实话,这只是治标不治本。如果状态真的很多,拆分才是正道。

2.4 实战建议:如何选择?

最后,我给大家一个决策树:

场景 推荐方案 原因
单个简单值(数字、字符串) useState 够用,简单
对象状态,更新逻辑简单 useState + 展开运算符 代码量少
对象状态,更新逻辑复杂 useReducer 逻辑集中,可测试
跨组件共享状态,层级浅 Context + useReducer 避免prop drilling
跨组件共享状态,层级深 考虑zustand/jotai Context性能问题

记住,没有银弹。我见过用Redux写todo list的,也见过用useState管理整个应用状态的。关键是:选择最适合当前场景的方案,而不是最流行的方案

好了,这一章就到这里。下一章我们会聊聊第三方状态库——从zustand到jotai,看看它们是怎么解决React原生状态的那些痛点的。