组件设计模式(上) 受控/非受控组件与容器组件

27 阅读3分钟

📚 概述

组件设计模式是 React 开发中的核心概念。理解受控/非受控组件以及容器组件模式,能帮助你写出更清晰、更可维护的代码。


1️⃣ 受控组件(Controlled Components)

受控组件是指表单数据由 React 状态管理的组件。

核心特点

  • ✅ 单一数据源:表单值存储在 state 中
  • ✅ 实时验证:可以在输入时进行验证
  • ✅ 强制格式:可以控制输入格式
  • ✅ 条件禁用:可以根据条件禁用提交按钮

代码示例:表单验证

import { useState } from 'react';

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

  const validateField = (name, value) => {
    switch (name) {
      case 'email':
        return /^[^\s@]+@[^\s@]+.[^\s@]+$/.test(value) 
          ? '' : '请输入有效的邮箱地址';
      case 'password':
        return value.length >= 8 
          ? '' : '密码至少需要 8 个字符';
      default:
        return '';
    }
  };

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

  const handleSubmit = (e) => {
    e.preventDefault();
    // 提交前验证所有字段
    const newErrors = {};
    Object.keys(formData).forEach(key => {
      const error = validateField(key, formData[key]);
      if (error) newErrors[key] = error;
    });

    if (Object.keys(newErrors).length === 0) {
      console.log('提交数据:', formData);
    } else {
      setErrors(newErrors);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label>用户名:</label>
        <input
          name="username"
          value={formData.username}
          onChange={handleChange}
        />
      </div>
      <div>
        <label>邮箱:</label>
        <input
          name="email"
          value={formData.email}
          onChange={handleChange}
        />
        {errors.email && <span style={{color: 'red'}}>{errors.email}</span>}
      </div>
      <div>
        <label>密码:</label>
        <input
          name="password"
          type="password"
          value={formData.password}
          onChange={handleChange}
        />
        {errors.password && <span style={{color: 'red'}}>{errors.password}</span>}
      </div>
      <button 
        type="submit"
        disabled={Object.values(errors).some(e => e) || !formData.username}
      >
        提交
      </button>
    </form>
  );
}

2️⃣ 非受控组件(Uncontrolled Components)

非受控组件是指表单数据由 DOM 自身管理的组件,使用 ref 来访问表单值。

适用场景

  • 📁 文件输入(<input type="file" />
  • 🔌 第三方库集成(不兼容 React 状态管理)
  • ⚡ 简单表单(不需要实时验证)
  • 📊 性能优化(避免频繁重渲染)

代码示例:使用 useRef

import { useRef } from 'react';

function UncontrolledForm() {
  const formRef = useRef(null);
  const fileInputRef = useRef(null);

  const handleSubmit = (e) => {
    e.preventDefault();
    
    // 通过 ref 获取表单数据
    const formData = new FormData(formRef.current);
    const data = Object.fromEntries(formData.entries());
    
    console.log('表单数据:', data);
    
    // 访问文件输入
    const file = fileInputRef.current.files[0];
    if (file) {
      console.log('选中的文件:', file.name);
    }
  };

  return (
    <form ref={formRef} onSubmit={handleSubmit}>
      <div>
        <label>用户名:</label>
        <input name="username" defaultValue="" />
      </div>
      <div>
        <label>邮箱:</label>
        <input name="email" type="email" defaultValue="" />
      </div>
      <div>
        <label>上传文件:</label>
        <input 
          ref={fileInputRef}
          name="file" 
          type="file" 
        />
      </div>
      <button type="submit">提交</button>
    </form>
  );
}

3️⃣ 容器组件模式(Container Component Pattern)

容器组件负责数据获取和业务逻辑,展示组件负责 UI 渲染。

核心思想

  • 🧠 容器组件:处理数据、状态、业务逻辑
  • 🎨 展示组件:只负责接收 props 并渲染 UI
  • 🔄 关注点分离:逻辑与视图解耦

代码示例:用户列表

// UserList.jsx - 展示组件(纯 UI)
function UserList({ users, loading, error, onRefresh }) {
  if (loading) return <div>加载中...</div>;
  if (error) return <div>错误:{error}</div>;

  return (
    <div>
      <button onClick={onRefresh}>刷新</button>
      <ul>
        {users.map(user => (
          <li key={user.id}>
            {user.name} - {user.email}
          </li>
        ))}
      </ul>
    </div>
  );
}

// UserListContainer.jsx - 容器组件(数据逻辑)
import { useState, useEffect } from 'react';
import UserList from './UserList';

function UserListContainer() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  const fetchUsers = async () => {
    try {
      setLoading(true);
      const response = await fetch('/api/users');
      const data = await response.json();
      setUsers(data);
      setError(null);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    fetchUsers();
  }, []);

  return (
    <UserList
      users={users}
      loading={loading}
      error={error}
      onRefresh={fetchUsers}
    />
  );
}

export default UserListContainer;

4️⃣ 现代替代方案:自定义 Hooks

随着 Hooks 的普及,自定义 Hooks 成为容器组件的现代替代方案。

代码示例:useUsers Hook

// hooks/useUsers.js
import { useState, useEffect } from 'react';

export function useUsers() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  const fetchUsers = async () => {
    try {
      setLoading(true);
      const response = await fetch('/api/users');
      const data = await response.json();
      setUsers(data);
      setError(null);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    fetchUsers();
  }, []);

  return { users, loading, error, refetch: fetchUsers };
}

// 使用 Hook 的组件
import { useUsers } from './hooks/useUsers';

function UserPage() {
  const { users, loading, error, refetch } = useUsers();

  if (loading) return <div>加载中...</div>;
  if (error) return <div>错误:{error}</div>;

  return (
    <div>
      <button onClick={refetch}>刷新</button>
      <ul>
        {users.map(user => (
          <li key={user.id}>{user.name} - {user.email}</li>
        ))}
      </ul>
    </div>
  );
}

💡 模式对比

特性受控组件非受控组件容器组件自定义 Hooks
数据源React stateDOMReact stateReact state
实时验证
性能中等中等
代码复用非常高
推荐场景表单验证文件输入/简单表单数据获取数据获取(现代)

⚠️ 选择建议

  1. 需要实时验证 → 受控组件
  2. 集成第三方库 → 非受控组件
  3. 复杂数据逻辑 → 自定义 Hooks(优先)或容器组件
  4. 简单表单 → 非受控组件(性能更好)