React18+快速入门 - 5.受控组件与非受控组件

React 受控组件与非受控组件详解

一、核心概念对比

1.1 受控组件 (Controlled Components)

  • 数据由React state控制:表单元素的值完全由React组件的state管理
  • 单向数据流:state → 表单值
  • 实时同步:每次用户输入都会触发state更新和组件重新渲染

1.2 非受控组件 (Uncontrolled Components)

  • 数据由DOM管理:表单元素的值由DOM自身维护
  • 按需获取:通过ref在需要时访问DOM值
  • 更接近原生HTML:性能更好,但集成度较低

二、受控组件详解

2.1 基本使用

jsx 复制代码
import React, { useState } from 'react';

function ControlledForm() {
  const [formData, setFormData] = useState({
    username: '',
    email: '',
    password: ''
  });

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

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('提交的数据:', formData);
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label>用户名:</label>
        <input
          type="text"
          name="username"
          value={formData.username}
          onChange={handleChange}
        />
      </div>
      
      <div>
        <label>邮箱:</label>
        <input
          type="email"
          name="email"
          value={formData.email}
          onChange={handleChange}
        />
      </div>
      
      <div>
        <label>密码:</label>
        <input
          type="password"
          name="password"
          value={formData.password}
          onChange={handleChange}
        />
      </div>
      
      <button type="submit">提交</button>
    </form>
  );
}

2.2 各种表单元素的受控实现

文本框
jsx 复制代码
const [text, setText] = useState('');
<input 
  type="text" 
  value={text} 
  onChange={(e) => setText(e.target.value)} 
/>
文本域
jsx 复制代码
const [content, setContent] = useState('');
<textarea 
  value={content} 
  onChange={(e) => setContent(e.target.value)} 
/>
单选按钮
jsx 复制代码
const [gender, setGender] = useState('male');
<div>
  <label>
    <input
      type="radio"
      value="male"
      checked={gender === 'male'}
      onChange={(e) => setGender(e.target.value)}
    />
    男
  </label>
  <label>
    <input
      type="radio"
      value="female"
      checked={gender === 'female'}
      onChange={(e) => setGender(e.target.value)}
    />
    女
  </label>
</div>
复选框
jsx 复制代码
// 单个复选框
const [agreed, setAgreed] = useState(false);
<input
  type="checkbox"
  checked={agreed}
  onChange={(e) => setAgreed(e.target.checked)}
/>

// 多个复选框
const [hobbies, setHobbies] = useState({
  reading: false,
  coding: false,
  gaming: false
});

const handleCheckboxChange = (e) => {
  const { name, checked } = e.target;
  setHobbies(prev => ({
    ...prev,
    [name]: checked
  }));
};
下拉选择框
jsx 复制代码
const [country, setCountry] = useState('china');
<select value={country} onChange={(e) => setCountry(e.target.value)}>
  <option value="china">中国</option>
  <option value="usa">美国</option>
  <option value="japan">日本</option>
</select>

// 多选
const [selectedCities, setSelectedCities] = useState([]);
<select 
  multiple 
  value={selectedCities} 
  onChange={(e) => {
    const options = Array.from(e.target.selectedOptions);
    setSelectedCities(options.map(option => option.value));
  }}
>
  <option value="beijing">北京</option>
  <option value="shanghai">上海</option>
  <option value="guangzhou">广州</option>
</select>

2.3 表单验证

jsx 复制代码
function ValidatedForm() {
  const [formData, setFormData] = useState({
    email: '',
    password: ''
  });
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});

  const validate = (name, value) => {
    const newErrors = { ...errors };
    
    if (name === 'email') {
      if (!value) {
        newErrors.email = '邮箱不能为空';
      } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
        newErrors.email = '邮箱格式不正确';
      } else {
        delete newErrors.email;
      }
    }
    
    if (name === 'password') {
      if (!value) {
        newErrors.password = '密码不能为空';
      } else if (value.length < 6) {
        newErrors.password = '密码至少6位';
      } else {
        delete newErrors.password;
      }
    }
    
    setErrors(newErrors);
  };

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData(prev => ({ ...prev, [name]: value }));
    
    // 实时验证
    validate(name, value);
  };

  const handleBlur = (e) => {
    const { name } = e.target;
    setTouched(prev => ({ ...prev, [name]: true }));
  };

  return (
    <form>
      <div>
        <input
          name="email"
          value={formData.email}
          onChange={handleChange}
          onBlur={handleBlur}
        />
        {touched.email && errors.email && (
          <span style={{ color: 'red' }}>{errors.email}</span>
        )}
      </div>
      
      <div>
        <input
          name="password"
          type="password"
          value={formData.password}
          onChange={handleChange}
          onBlur={handleBlur}
        />
        {touched.password && errors.password && (
          <span style={{ color: 'red' }}>{errors.password}</span>
        )}
      </div>
    </form>
  );
}

2.4 动态表单字段

jsx 复制代码
function DynamicForm() {
  const [fields, setFields] = useState([{ value: '' }]);

  const addField = () => {
    setFields([...fields, { value: '' }]);
  };

  const removeField = (index) => {
    setFields(fields.filter((_, i) => i !== index));
  };

  const handleFieldChange = (index, value) => {
    const newFields = [...fields];
    newFields[index].value = value;
    setFields(newFields);
  };

  return (
    <div>
      {fields.map((field, index) => (
        <div key={index}>
          <input
            value={field.value}
            onChange={(e) => handleFieldChange(index, e.target.value)}
          />
          {fields.length > 1 && (
            <button type="button" onClick={() => removeField(index)}>
              删除
            </button>
          )}
        </div>
      ))}
      <button type="button" onClick={addField}>
        添加字段
      </button>
    </div>
  );
}

三、非受控组件详解

3.1 基本使用

jsx 复制代码
import React, { useRef } from 'react';

function UncontrolledForm() {
  const usernameRef = useRef();
  const emailRef = useRef();
  const fileRef = useRef();

  const handleSubmit = (e) => {
    e.preventDefault();
    
    const formData = {
      username: usernameRef.current.value,
      email: emailRef.current.value,
      // 文件输入特别适合使用非受控组件
      file: fileRef.current.files[0]
    };
    
    console.log('表单数据:', formData);
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label>用户名:</label>
        <input 
          type="text" 
          ref={usernameRef} 
          defaultValue="默认用户名" 
        />
      </div>
      
      <div>
        <label>邮箱:</label>
        <input 
          type="email" 
          ref={emailRef} 
        />
      </div>
      
      <div>
        <label>上传文件:</label>
        <input 
          type="file" 
          ref={fileRef} 
        />
      </div>
      
      <button type="submit">提交</button>
    </form>
  );
}

3.2 各种表单元素的非受控实现

使用 defaultValue
jsx 复制代码
function UncontrolledInputs() {
  const inputRef = useRef();
  const textareaRef = useRef();
  const selectRef = useRef();

  return (
    <div>
      {/* 文本框 */}
      <input 
        type="text" 
        ref={inputRef} 
        defaultValue="默认文本" 
      />
      
      {/* 文本域 */}
      <textarea 
        ref={textareaRef} 
        defaultValue="默认内容" 
      />
      
      {/* 下拉选择 */}
      <select ref={selectRef} defaultValue="option2">
        <option value="option1">选项1</option>
        <option value="option2">选项2</option>
      </select>
      
      {/* 复选框 */}
      <input 
        type="checkbox" 
        defaultChecked 
        ref={useRef()} 
      />
      
      {/* 单选按钮 */}
      <input 
        type="radio" 
        defaultChecked 
        ref={useRef()} 
      />
    </div>
  );
}

3.3 文件上传(非受控的典型用例)

jsx 复制代码
function FileUpload() {
  const fileInputRef = useRef();

  const handleSubmit = async (e) => {
    e.preventDefault();
    const file = fileInputRef.current.files[0];
    
    if (!file) {
      alert('请选择文件');
      return;
    }
    
    // 创建FormData对象
    const formData = new FormData();
    formData.append('file', file);
    
    // 上传文件
    try {
      const response = await fetch('/api/upload', {
        method: 'POST',
        body: formData
      });
      const result = await response.json();
      console.log('上传成功:', result);
    } catch (error) {
      console.error('上传失败:', error);
    }
  };

  const handlePreview = () => {
    const file = fileInputRef.current.files[0];
    if (file) {
      const reader = new FileReader();
      reader.onload = (e) => {
        // 预览图片
        const img = document.createElement('img');
        img.src = e.target.result;
        document.body.appendChild(img);
      };
      reader.readAsDataURL(file);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="file"
        ref={fileInputRef}
        accept="image/*, .pdf, .doc"
        multiple  // 支持多文件上传
      />
      <button type="button" onClick={handlePreview}>
        预览
      </button>
      <button type="submit">上传</button>
    </form>
  );
}

3.4 第三方库集成

jsx 复制代码
import React, { useEffect, useRef } from 'react';
import DatePicker from 'some-datepicker-library';

function ThirdPartyIntegration() {
  const datePickerRef = useRef();
  const editorRef = useRef();

  useEffect(() => {
    // 初始化第三方日期选择器
    const datePicker = new DatePicker(datePickerRef.current, {
      format: 'YYYY-MM-DD'
    });
    
    // 初始化富文本编辑器
    const editor = new RichTextEditor(editorRef.current);
    
    return () => {
      // 清理
      datePicker.destroy();
      editor.destroy();
    };
  }, []);

  const getValues = () => {
    return {
      date: datePickerRef.current.value,
      content: editorRef.current.innerHTML
    };
  };

  return (
    <div>
      <input type="text" ref={datePickerRef} />
      <div ref={editorRef}></div>
    </div>
  );
}

四、混合使用场景

4.1 受控与非受控结合

jsx 复制代码
function HybridForm() {
  // 受控状态
  const [userInfo, setUserInfo] = useState({
    name: '',
    age: ''
  });
  
  // 非受控引用
  const fileInputRef = useRef();
  const colorPickerRef = useRef();

  const handleSubmit = (e) => {
    e.preventDefault();
    
    const formData = {
      ...userInfo,
      file: fileInputRef.current.files[0],
      color: colorPickerRef.current.value
    };
    
    console.log('完整表单数据:', formData);
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* 受控组件 */}
      <input
        type="text"
        value={userInfo.name}
        onChange={(e) => setUserInfo({...userInfo, name: e.target.value})}
        placeholder="姓名"
      />
      
      <input
        type="number"
        value={userInfo.age}
        onChange={(e) => setUserInfo({...userInfo, age: e.target.value})}
        placeholder="年龄"
      />
      
      {/* 非受控组件 */}
      <input
        type="file"
        ref={fileInputRef}
      />
      
      <input
        type="color"
        ref={colorPickerRef}
        defaultValue="#000000"
      />
      
      <button type="submit">提交</button>
    </form>
  );
}

4.2 性能优化:debounce受控输入

jsx 复制代码
import React, { useState, useCallback } from 'react';
import { debounce } from 'lodash';

function DebouncedInput() {
  const [value, setValue] = useState('');
  const [searchResult, setSearchResult] = useState('');
  
  // 防抖的搜索函数
  const debouncedSearch = useCallback(
    debounce((searchTerm) => {
      console.log('搜索:', searchTerm);
      // 模拟API调用
      setSearchResult(`搜索结果: ${searchTerm}`);
    }, 500),
    []
  );

  const handleChange = (e) => {
    const newValue = e.target.value;
    setValue(newValue);
    debouncedSearch(newValue);
  };

  return (
    <div>
      <input
        type="text"
        value={value}
        onChange={handleChange}
        placeholder="输入搜索关键词..."
      />
      <div>{searchResult}</div>
    </div>
  );
}

五、最佳实践与选择指南

5.1 何时使用受控组件

使用场景:

  1. 需要实时验证输入
  2. 表单提交前需要验证
  3. 表单字段相互依赖
  4. 需要动态禁用/启用提交按钮
  5. 需要强制特定格式的输入

5.2 何时使用非受控组件

使用场景:

  1. 文件上传 <input type="file">
  2. 集成第三方DOM库
  3. 大型表单性能优化
  4. 简单的、不需要验证的输入
  5. 与现有非React代码集成

5.3 性能对比

jsx 复制代码
// 受控组件:每次输入都重新渲染
function ControlledPerformance() {
  const [value, setValue] = useState('');
  console.log('受控组件重新渲染'); // 每次输入都会打印
  
  return <input value={value} onChange={(e) => setValue(e.target.value)} />;
}

// 非受控组件:只在需要时获取值
function UncontrolledPerformance() {
  const inputRef = useRef();
  console.log('非受控组件渲染'); // 只在初始渲染时打印
  
  const getValue = () => {
    console.log('获取值:', inputRef.current.value);
  };
  
  return (
    <>
      <input ref={inputRef} defaultValue="" />
      <button onClick={getValue}>获取值</button>
    </>
  );
}

5.4 自定义Hook封装

jsx 复制代码
// 自定义受控表单Hook
function useForm(initialValues = {}) {
  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});

  const handleChange = (e) => {
    const { name, value, type, checked } = e.target;
    const newValue = type === 'checkbox' ? checked : value;
    
    setValues(prev => ({
      ...prev,
      [name]: newValue
    }));
    
    // 清除该字段的错误
    if (errors[name]) {
      setErrors(prev => ({
        ...prev,
        [name]: ''
      }));
    }
  };

  const handleBlur = (e) => {
    const { name } = e.target;
    setTouched(prev => ({
      ...prev,
      [name]: true
    }));
  };

  const resetForm = () => {
    setValues(initialValues);
    setErrors({});
    setTouched({});
  };

  return {
    values,
    errors,
    touched,
    handleChange,
    handleBlur,
    resetForm,
    setValues,
    setErrors
  };
}

// 使用自定义Hook
function SmartForm() {
  const {
    values,
    errors,
    touched,
    handleChange,
    handleBlur,
    resetForm
  } = useForm({
    username: '',
    email: ''
  });

  return (
    <form>
      <input
        name="username"
        value={values.username}
        onChange={handleChange}
        onBlur={handleBlur}
      />
      {touched.username && errors.username && (
        <div>{errors.username}</div>
      )}
      
      <button type="button" onClick={resetForm}>
        重置
      </button>
    </form>
  );
}

总结

核心要点:

  1. 受控组件:React state作为唯一数据源,实时同步,适合需要验证和控制的场景
  2. 非受控组件:DOM作为数据源,按需获取,适合文件上传和性能敏感场景
  3. 混合使用:根据实际需求选择最合适的方式
  4. 自定义Hook:封装表单逻辑,提高代码复用性

选择建议:

  • 大多数情况下,优先使用受控组件,因为更符合React数据流理念
  • 文件上传和第三方库集成时,使用非受控组件
  • 性能关键的大型表单,可以考虑非受控组件或防抖优化
  • 结合实际需求,可以混合使用两种方式

通过理解这两种模式的特点和适用场景,你可以根据具体需求选择最合适的方式,构建出既高效又易维护的React表单应用。

相关推荐
古韵19 小时前
一个让你爽歪歪的请求工具是怎样的
vue.js·react.js·node.js
zhangyao94033019 小时前
View Design TimePicker 限制时间范围
前端·javascript·view design
成都证图科技有限公司19 小时前
安卓系统Chrome内核:Android System WebView
android·前端·chrome
Jayson柴19 小时前
对静态资源进行hash命名,可以选择哪些方式?
前端·哈希算法
快乐点吧19 小时前
为啥不用Webpack
前端·webpack·node.js
yeshihouhou19 小时前
redis(zset使用)使用场景案例
前端·redis·python
IT_陈寒19 小时前
从混乱到优雅:这5个现代JavaScript技巧让你的代码性能提升50%
前端·人工智能·后端