4、React状态管理入门:useState Hook详解、状态提升与共享、受控组件与非受控组件、表单处理实战

好,咱们今天聊聊React里的状态管理。说实话,很多新手刚接触React时,最困惑的就是「状态」到底是个什么东西。我当年也一样,看着官方文档里那个计数器例子,心里想:这不就是个变量吗?搞这么复杂干嘛?

后来我才明白,状态和普通变量的区别,就在于它跟UI是绑定的。你改了状态,React会自动帮你更新视图。嗯,这才是核心。

4.1 useState Hook:最基础的状态管理

先看个最简单的例子。咱们用函数组件加Hook的方式,写一个计数器:

import React, { useState } from 'react';

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

  return (
    <div>
      <p>当前计数:{count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}

这里有几个关键点,我跟你掰扯清楚:

  • useState(0):括号里的0是初始值。可以是数字、字符串、对象、数组,啥都行。
  • 返回值是个数组:第一个是当前状态值,第二个是更新函数。用解构赋值拿过来用。
  • setCount:调用它,React会重新渲染组件,并且把新值赋给count。
我的小习惯:命名时,状态变量用名词,更新函数用set+名词。比如user/setUser、list/setList。团队里一看就懂,不用猜。

但有个坑,我踩过好几次。你想想看,如果连续调用两次setCount会怎样?

function handleClick() {
  setCount(count + 1);
  setCount(count + 1);
}

结果不是加2,而是加1。为什么?因为React的setState是异步的,两次调用拿到的count是同一个值。正确的做法是用函数式更新:

function handleClick() {
  setCount(prev => prev + 1);
  setCount(prev => prev + 1);
}

这样每次都能拿到最新的值,结果就是加2。嗯,这个细节很重要。

4.2 状态提升:兄弟组件怎么共享数据?

实际项目中,你经常会遇到这种情况:两个兄弟组件需要共享同一个状态。比如一个输入框组件和一个展示组件,输入的内容要实时显示。

React的哲学是「单向数据流」。子组件不能直接改父组件的状态,但父组件可以把状态和更新函数一起传给子组件。这就是状态提升。

看个例子:

function Parent() {
  const [text, setText] = useState('');

  return (
    <div>
      <InputBox value={text} onChange={setText} />
      <DisplayBox text={text} />
    </div>
  );
}

function InputBox({ value, onChange }) {
  return (
    <input
      value={value}
      onChange={(e) => onChange(e.target.value)}
    />
  );
}

function DisplayBox({ text }) {
  return <p>你输入的是:{text}</p>;
}

你看,状态text定义在Parent里,通过props传给两个子组件。InputBox负责改,DisplayBox负责读。数据流清晰,谁都不越界。

核心原则:状态尽量放在离它最近的公共父组件里。别一股脑全塞到顶层,也别每个组件自己存一份。

4.3 受控组件 vs 非受控组件

这两个概念,说白了就是「表单数据归谁管」的问题。

受控组件:表单数据由React状态控制。你输入一个字,触发onChange,更新state,state再回流到value里。整个过程React全程参与。

function ControlledInput() {
  const [name, setName] = useState('');

  return (
    <input
      value={name}
      onChange={(e) => setName(e.target.value)}
    />
  );
}

非受控组件:表单数据由DOM自己管理。你想取值的时候,用ref去拿。

function UncontrolledInput() {
  const inputRef = useRef(null);

  function handleSubmit() {
    alert(inputRef.current.value);
  }

  return (
    <div>
      <input ref={inputRef} defaultValue="默认值" />
      <button onClick={handleSubmit}>提交</button>
    </div>
  );
}
对比项 受控组件 非受控组件
数据来源 React state DOM自身
取值方式 直接从state读 通过ref获取
适用场景 需要实时校验、联动、格式化 简单表单、文件上传
代码量 稍多 较少
我曾经踩过的坑:做文件上传时,用了受控组件,结果value始终是空字符串。因为安全原因,浏览器不允许JS设置文件输入框的value。所以文件上传必须用非受控组件。

4.4 表单处理实战:一个完整的注册表单

理论说完了,咱们来点实战。写一个注册表单,包含用户名、密码、确认密码、性别选择、兴趣爱好。

function RegisterForm() {
  const [formData, setFormData] = useState({
    username: '',
    password: '',
    confirmPassword: '',
    gender: 'male',
    hobbies: []
  });

  const [errors, setErrors] = useState({});

  function handleChange(e) {
    const { name, value, type, checked } = e.target;

    if (type === 'checkbox') {
      // 处理多选框
      const updatedHobbies = checked
        ? [...formData.hobbies, value]
        : formData.hobbies.filter(h => h !== value);

      setFormData(prev => ({ ...prev, hobbies: updatedHobbies }));
    } else {
      setFormData(prev => ({ ...prev, [name]: value }));
    }
  }

  function validate() {
    const newErrors = {};

    if (!formData.username.trim()) {
      newErrors.username = '用户名不能为空';
    }
    if (formData.password.length < 6) {
      newErrors.password = '密码至少6位';
    }
    if (formData.password !== formData.confirmPassword) {
      newErrors.confirmPassword = '两次密码不一致';
    }

    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  }

  function handleSubmit(e) {
    e.preventDefault();
    if (validate()) {
      console.log('提交数据:', formData);
      // 这里调用API
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label>用户名:</label>
        <input
          name="username"
          value={formData.username}
          onChange={handleChange}
        />
        {errors.username && <span style={{color: 'red'}}>{errors.username}</span>}
      </div>

      <div>
        <label>密码:</label>
        <input
          type="password"
          name="password"
          value={formData.password}
          onChange={handleChange}
        />
        {errors.password && <span style={{color: 'red'}}>{errors.password}</span>}
      </div>

      <div>
        <label>确认密码:</label>
        <input
          type="password"
          name="confirmPassword"
          value={formData.confirmPassword}
          onChange={handleChange}
        />
        {errors.confirmPassword && <span style={{color: 'red'}}>{errors.confirmPassword}</span>}
      </div>

      <div>
        <label>性别:</label>
        <select name="gender" value={formData.gender} onChange={handleChange}>
          <option value="male">男</option>
          <option value="female">女</option>
        </select>
      </div>

      <div>
        <label>兴趣爱好:</label>
        <label>
          <input
            type="checkbox"
            value="reading"
            checked={formData.hobbies.includes('reading')}
            onChange={handleChange}
          /> 阅读
        </label>
        <label>
          <input
            type="checkbox"
            value="coding"
            checked={formData.hobbies.includes('coding')}
            onChange={handleChange}
          /> 编程
        </label>
      </div>

      <button type="submit">注册</button>
    </form>
  );
}

这个例子涵盖了大部分表单场景。有几个点我想强调一下:

  • 统一的事件处理函数:通过name属性区分不同字段,避免写一堆重复的onChange。
  • 复选框的特殊处理:因为是多选,需要维护一个数组,选中时添加,取消时移除。
  • 表单验证:提交前校验,错误信息存在errors对象里,实时展示。
我的建议:如果表单字段超过5个,或者有复杂的联动逻辑,就别手写了。用Formik或React Hook Form这类库,能省不少事。但学习阶段,手写一遍能帮你理解底层原理。

好了,这一章的内容就到这儿。useState是React状态管理的基石,状态提升是组件通信的常用手段,受控组件是React表单的核心模式。把这些搞明白,后面学useReducer、Context、Redux就会轻松很多。

下一章,咱们聊聊useEffect和副作用处理。到时候见。