useState

函数组件的状态管理基石 - 让组件拥有"记忆"能力

核心概述

在 React 函数组件诞生之前,类组件通过 this.state 管理组件内部状态。 但当你转向函数组件后,会发现一个根本性问题:函数每次渲染都会重新执行, 这意味着函数内的局部变量无法在渲染之间保留。

useState 正是解决这个问题的答案。它为函数组件引入了一个"持久化存储空间"—— React 会在组件的多次渲染之间保留这个值,并在状态变化时触发组件重新渲染。 更重要的是,它让状态管理变得可预测:每次状态更新都会创建一个新的渲染"快照", 而不是直接修改现有状态。

适用场景:当组件需要在多次渲染间保存和更新数据时使用 useState。 典型场景包括用户输入、UI 切换状态、从服务器获取的数据等。 但如果你需要存储复杂的衍生状态或需要避免不必要的渲染,可能需要考虑 useReduceruseMemo

💡 心智模型

将 useState 想象成一个"带触发器的口袋":

  • 口袋(state): 存放你的东西(状态值)
  • 标签(setter): 用来替换口袋里的东西
  • 铃声(re-render): 替换东西时响起,通知组件"内容变了,重新显示"

重要的一点:你不能直接从口袋里拿出东西修改,必须用标签换取新东西。

技术规格

类型签名

TypeScript
function useState<S>(
  initialState: S | (() => S)
): [S, Dispatch<SetStateAction<S>>]

参数说明

参数类型说明
initialStateS | (() => S)初始状态值,或返回初始值的函数(惰性初始化)。 这个值只在组件首次渲染时使用。

返回值

返回值类型说明
数组第一个元素S当前的状态值
数组第二个元素Dispatch<SetStateAction<S>>状态更新函数。调用它会触发组件重新渲染, 并将状态值更新为新值

运行机制

初始化阶段:React 在组件首次渲染时执行 useState(initialState), 将初始值存储在内部的状态链表中(Fiber 节点的 memoizedState)。

更新阶段:当你调用 setter 函数时,React 不会立即更新状态。 而是将更新操作加入更新队列,并在当前渲染周期完成后安排一次新的渲染。 在下次渲染时,React 从内部状态链表中读取最新值,确保组件看到的是最新的状态。

批处理优化:在 React 18+ 的自动批处理机制下, 多个 setState 调用会被合并为一次重新渲染,这是通过优先级调度和批处理实现的性能优化。

实战演练

1. 基础用法

最简洁的状态声明,展示核心语法结构:

TypeScript
// 数字状态
const [count, setCount] = useState(0);

// 字符串状态
const [name, setName] = useState('Guest');

// 布尔状态
const [isVisible, setIsVisible] = useState(false);

// 对象状态
const [user, setUser] = useState({ name: '', age: 0 });

2. 生产级案例:表单状态管理

处理真实业务场景中的表单状态,包含完整的类型定义和最佳实践:

TypeScript
import { useState, ChangeEvent } from 'react';

// 定义表单数据类型
interface FormData {
  email: string;
  password: string;
  rememberMe: boolean;
}

// 定义错误信息类型
interface FormErrors {
  email?: string;
  password?: string;
}

export function LoginForm() {
  // 初始化表单状态
  const [formData, setFormData] = useState<FormData>({
    email: '',
    password: '',
    rememberMe: false,
  });

  // 初始化错误状态
  const [errors, setErrors] = useState<FormErrors>({});
  const [isSubmitting, setIsSubmitting] = useState(false);

  // 处理输入变化
  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
    const { name, value, type, checked } = e.target;

    // 使用函数式更新确保基于最新状态
    setFormData(prev => ({
      ...prev,
      [name]: type === 'checkbox' ? checked : value,
    }));

    // 实时清除错误
    if (errors[name as keyof FormErrors]) {
      setErrors(prev => ({
        ...prev,
        [name]: undefined,
      }));
    }
  };

  // 表单提交
  const handleSubmit = async (e: ChangeEvent<HTMLFormElement>) => {
    e.preventDefault();

    // 验证表单
    const newErrors: FormErrors = {};
    if (!formData.email) {
      newErrors.email = '邮箱不能为空';
    }
    if (!formData.password) {
      newErrors.password = '密码不能为空';
    }

    if (Object.keys(newErrors).length > 0) {
      setErrors(newErrors);
      return;
    }

    // 提交逻辑
    setIsSubmitting(true);
    try {
      await login(formData.email, formData.password);
    } catch (err) {
      setErrors({ email: '登录失败,请检查邮箱和密码' });
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* 表单字段... */}
    </form>
  );
}

3. 生产级案例:惰性初始化

当初始状态需要通过复杂计算获得时,使用惰性初始化避免重复计算:

TypeScript
import { useState } from 'react';

// ❌ 错误:每次渲染都会执行 expensiveComputation
const [state, setState] = useState(expensiveComputation());

// ✅ 正确:只在首次渲染时执行一次
const [state, setState] = useState(() => {
  console.log('只会在首次渲染时执行');
  return expensiveComputation();
});

// 实际应用:从 localStorage 读取初始值
const [userPreferences, setUserPreferences] = useState(() => {
  try {
    const saved = localStorage.getItem('user-preferences');
    return saved ? JSON.parse(saved) : defaultPreferences;
  } catch {
    return defaultPreferences;
  }
});

避坑指南

陷阱 1: 直接修改状态对象

问题:JavaScript 的对象是引用类型,直接修改不会触发重新渲染。

TypeScript
// ❌ 错误做法
const [user, setUser] = useState({ name: 'John', age: 30 });

// 直接修改 - React 检测不到变化!
user.age = 31;
setUser(user);
TypeScript
// ✅ 正确做法:创建新对象
const [user, setUser] = useState({ name: 'John', age: 30 });

// 使用展开运算符创建新对象
setUser({ ...user, age: 31 });

// 或使用 Object.assign
setUser(Object.assign({}, user, { age: 31 }));

陷阱 2: 闭包陷阱(Stale Closure)

问题:在事件处理函数或定时器中直接读取状态, 可能会读取到旧的闭包值。

TypeScript
// ❌ 问题代码
function Counter() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    // 这里的 count 永远是 0,因为函数创建时的值被闭包捕获了
    setTimeout(() => {
      console.log(count); // 0
      setCount(count + 1); // 总是设置为 1
    }, 3000);
  };

  return <button onClick={handleClick}>增加</button>;
}
TypeScript
// ✅ 解决方案:使用函数式更新
function Counter() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setTimeout(() => {
      // 函数式更新会接收最新状态
      setCount(prev => prev + 1);
    }, 3000);
  };

  return <button onClick={handleClick}>增加</button>;
}

核心原则:只要新状态依赖于旧状态,就必须使用函数式更新 setState(prev => ...)

陷阱 3: 期望 setState 后立即生效

问题:setState 是异步的,调用后状态不会立即改变。

TypeScript
// ❌ 错误期望
const [count, setCount] = useState(0);

const handleClick = () => {
  setCount(count + 1);
  console.log(count); // 还是 0! setState 是异步的
};
 
// ✅ 正确做法:如果需要基于新状态计算,使用函数式更新
const handleClick = () => {
  setCount(prev => {
    const newValue = prev + 1;
    console.log(newValue); // 这是正确的值
    return newValue;
  });
};

陷阱 4: 在渲染过程中更新状态

问题:在组件渲染过程中直接调用 setState 会导致无限循环。

// ❌ 致命错误:无限循环
function Component({ value }) {
  const [data, setData] = useState(value);

  // 在渲染中调用 setState -> 无限重新渲染
  if (data !== value) {
    setData(value);
  }

  return <div>{data}</div>;
}
// ✅ 解决方案:使用 useEffect 同步外部变化
function Component({ value }) {
  const [data, setData] = useState(value);

  // 只在 value 变化时更新
  useEffect(() => {
    setData(value);
  }, [value]);

  return <div>{data}</div>;
}

最佳实践

✅ 推荐模式

  • 使用函数式更新处理依赖旧状态的逻辑
  • 为复杂状态使用 TypeScript 接口定义
  • 多个相关状态考虑合并为一个对象状态
  • 使用惰性初始化避免昂贵的计算
  • 保持状态扁平化,避免深层嵌套

❌ 避免模式

  • 不要直接修改状态对象或数组
  • 不要在渲染中调用 setState
  • 不要在 useEffect 中缺少依赖的状态更新
  • 不要过度使用 state(能用 props 就不用 state)
  • 不要忘记清理副作用(如定时器)

性能优化

虽然 useState 本身很高效,但在某些场景下需要注意性能问题:

1. 状态拆分避免不必要的渲染

将频繁变化的状态和稳定的状态拆分,可以减少组件重渲染范围:

// ✅ 好的做法
const [name, setName] = useState('');
const [age, setAge] = useState(0);
// name 变化不会影响 age 的渲染

2. 使用 useReducer 处理复杂状态逻辑

当状态更新逻辑复杂,或下一个状态依赖于前一个状态时:

// 更适合用 useReducer 的场景
const [state, dispatch] = useReducer(reducer, initialState);

3. 将派生状态移出 useState

如果某个状态可以通过其他状态计算得出,不要用 useState 存储:

// ❌ 冗余状态
const [items, setItems] = useState([]);
const [count, setCount] = useState(0);
// count 实际上等于 items.length

// ✅ 使用 useMemo 计算派生状态
const [items, setItems] = useState([]);
const count = useMemo(() => items.length, [items]);

延伸阅读