Back to Blog
· 7 min read

React 设计模式指北

React

🤯(看到公司项目中一个组件将近两千行时的我belike…)所有逻辑混在一起,修改一个功能要翻半天,还得全局定位调用点,避免影响到其它功能,这就是缺乏设计模式的后果。良好的设计模式能让代码结构清晰、易于维护,也是进阶高级开发的必经之路。

React 设计模式是经过实践验证的代码组织方式。掌握这些模式,能让代码更清晰、更易维护。

1. 容器组件与展示组件

1.1 核心概念

这种模式将业务逻辑和 UI 呈现分离:

// 展示组件:只负责渲染
function UserList({ users, onDelete }) {
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>
          {user.name}
          <button onClick={() => onDelete(user.id)}>删除</button>
        </li>
      ))}
    </ul>
  );
}

// 容器组件:负责数据获取和状态管理
function UserListContainer() {
  const [users, setUsers] = useState([]);

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

  const handleDelete = async (id) => {
    await deleteUser(id);
    setUsers(users.filter(u => u.id !== id));
  };

  return <UserList users={users} onDelete={handleDelete} />;
}
特点展示组件容器组件
职责UI 渲染数据逻辑
状态无状态有状态
复用性

2. 高阶组件(HOC)

2.1 基本用法

高阶组件是接收组件,返回新组件的函数:

function withLoading(Component) {
  return function WithLoading({ isLoading, ...props }) {
    if (isLoading) {
      return <Spinner />;
    }
    return <Component {...props} />;
  };
}

// 使用
const UserListWithLoading = withLoading(UserList);

function App() {
  return <UserListWithLoading isLoading={true} users={[]} />;
}

2.2 应用场景:权限控制

function withAuth(Component) {
  return function AuthenticatedComponent({ isAuthenticated, ...props }) {
    if (!isAuthenticated) {
      return <Navigate to="/login" />;
    }
    return <Component {...props} />;
  };
}

const ProtectedRoute = withAuth(Dashboard);

注意: Hooks 出现后,HOC 的使用场景减少了很多。优先考虑自定义 Hook。

3. 渲染属性(Render Props)

3.1 模式定义

通过函数 prop 共享组件内部状态:

function MousePosition({ render }) {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    const handleMove = (e) => setPosition({ x: e.clientX, y: e.clientY });
    window.addEventListener('mousemove', handleMove);
    return () => window.removeEventListener('mousemove', handleMove);
  }, []);

  return render(position);
}

// 使用
<MousePosition render={({ x, y }) => (
  <div>鼠标位置: {x}, {y}</div>
)} />

3.2 对比 HOC

特性HOCRender Props
代码组织包装组件传递渲染函数
灵活性
TypeScript较复杂更友好

4. Hooks 模式

4.1 useState 和 useEffect

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

  useEffect(() => {
    document.title = `计数: ${count}`;
    return () => document.title = 'React App';
  }, [count]);

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

4.2 useContext 实现跨组件通信

const ThemeContext = createContext('light');

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar() {
  const theme = useContext(ThemeContext);
  return <button className={theme}>主题按钮</button>;
}

4.3 自定义 Hook 复用逻辑

// 封装数据获取逻辑
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    setLoading(true);
    fetch(url)
      .then(res => res.json())
      .then(setData)
      .catch(setError)
      .finally(() => setLoading(false));
  }, [url]);

  return { data, loading, error };
}

// 使用
function UserProfile({ userId }) {
  const { data, loading, error } = useFetch(`/api/users/${userId}`);

  if (loading) return <Spinner />;
  if (error) return <Error message={error.message} />;

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

4.4 useReducer 管理复杂状态

function todoReducer(state, action) {
  switch (action.type) {
    case 'ADD_TODO':
      return [...state, { id: Date.now(), text: action.text, done: false }];
    case 'TOGGLE_TODO':
      return state.map(todo =>
        todo.id === action.id ? { ...todo, done: !todo.done } : todo
      );
    case 'DELETE_TODO':
      return state.filter(todo => todo.id !== action.id);
    default:
      return state;
  }
}

function TodoApp() {
  const [todos, dispatch] = useReducer(todoReducer, []);

  return (
    <div>
      <button onClick={() => dispatch({ type: 'ADD_TODO', text: '新任务' })}>
        添加任务
      </button>
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.done}
              onChange={() => dispatch({ type: 'TOGGLE_TODO', id: todo.id })}
            />
            {todo.text}
          </li>
        ))}
      </ul>
    </div>
  );
}

5. 组件复合(Composition)

5.1 children prop

function Card({ children, title }) {
  return (
    <div className="card">
      {title && <h2>{title}</h2>}
      <div className="card-body">{children}</div>
    </div>
  );
}

function App() {
  return (
    <Card title="用户信息">
      <p>姓名: 张三</p>
      <p>邮箱: zhangsan@example.com</p>
    </Card>
  );
}

5.2 render prop / slot

function Modal({ isOpen, title, children, footer }) {
  if (!isOpen) return null;

  return (
    <div className="modal-overlay">
      <div className="modal-content">
        <h2>{title}</h2>
        <div className="modal-body">{children}</div>
        <div className="modal-footer">{footer}</div>
      </div>
    </div>
  );
}

function App() {
  return (
    <Modal
      isOpen={true}
      title="确认删除"
      footer={
        <button onClick={handleConfirm}>确认</button>
      }
    >
      确定要删除这个项目吗?
    </Modal>
  );
}

6. 错误边界

6.1 实现错误边界

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    logErrorToService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback || <div>出现错误</div>;
    }
    return this.props.children;
  }
}

// 使用
<ErrorBoundary fallback={<ErrorPage />}>
  <MyComponent />
</ErrorBoundary>

注意: 错误边界只捕获子组件的错误,不能捕获自身的错误。

7. 受控与非受控组件

7.1 受控组件

function ControlledInput() {
  const [value, setValue] = useState('');

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

7.2 非受控组件

非受控组件的状态由 DOM 自身管理,React 只通过 ref 获取值:

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

  const handleSubmit = () => {
    console.log(inputRef.current.value);
  };

  return (
    <div>
      <input ref={inputRef} defaultValue="初始值" />
      <button onClick={handleSubmit}>提交</button>
    </div>
  );
}

7.3 核心区别

特性受控组件非受控组件
数据来源React 状态DOM 自身
更新方式通过 props/onChange通过 ref 获取
初始值value propdefaultValue prop
代码量较多较少
// 受控组件:React 完全控制
<input value={value} onChange={e => setValue(e.target.value)} />

// 非受控组件:DOM 控制
<input ref={inputRef} defaultValue="默认值" />

7.4 何时使用哪种

使用受控组件

  • 需要对输入进行验证
  • 需要根据输入值触发其他更新
  • 需要禁用/格式化输入
  • 需要与其他组件共享状态
// 受控组件:实时验证
function ValidatedInput() {
  const [value, setValue] = useState('');
  const [error, setError] = useState('');

  const handleChange = (e) => {
    const val = e.target.value;
    setValue(val);
    setError(val.length < 3 ? '至少需要3个字符' : '');
  };

  return (
    <div>
      <input value={value} onChange={handleChange} />
      {error && <span className="error">{error}</span>}
    </div>
  );
}

使用非受控组件

  • 只需要获取最终值
  • 不需要实时处理输入
  • 与第三方库集成
  • 简单的表单提交
// 非受控组件:简单的表单提交
function SimpleForm() {
  const nameRef = useRef(null);
  const emailRef = useRef(null);

  const handleSubmit = (e) => {
    e.preventDefault();
    const formData = {
      name: nameRef.current.value,
      email: emailRef.current.value
    };
    submitToAPI(formData);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input ref={nameRef} defaultValue="" />
      <input ref={emailRef} defaultValue="" />
      <button type="submit">提交</button>
    </form>
  );
}

7.5 表单场景对比

// 场景:搜索输入框
// 搜索建议需要实时更新 → 受控组件
function SearchInput() {
  const [query, setQuery] = useState('');
  const [suggestions, setSuggestions] = useState([]);

  useEffect(() => {
    if (query) {
      fetchSuggestions(query).then(setSuggestions);
    }
  }, [query]);

  return (
    <div>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <SuggestionsList items={suggestions} />
    </div>
  );
}

// 场景:联系表单
// 只需要提交时获取数据 → 非受控组件
function ContactForm() {
  const nameRef = useRef(null);
  const messageRef = useRef(null);

  const handleSubmit = () => {
    sendEmail({
      name: nameRef.current.value,
      message: messageRef.current.value
    });
  };

  return (
    <form>
      <input ref={nameRef} />
      <textarea ref={messageRef} />
      <button onClick={handleSubmit}>发送</button>
    </form>
  );
}

7.6 ref 转发

当需要为自定义组件添加 ref 时,使用 forwardRef

// 父组件
function Parent() {
  const inputRef = useRef(null);

  const handleClick = () => {
    inputRef.current.focus();
  };

  return (
    <div>
      <CustomInput ref={inputRef} />
      <button onClick={handleClick}>聚焦输入框</button>
    </div>
  );
}

// 子组件:使用 forwardRef
const CustomInput = forwardRef((props, ref) => {
  return <input ref={ref} {...props} />;
});

7.7 常见陷阱

// ❌ 错误:同时使用 value 和 defaultValue
<input value={value} defaultValue="默认值" />

// ✅ 正确:只使用一种
<input value={value} onChange={handleChange} />
// 或
<input defaultValue="默认值" />

// ❌ 错误:受控组件不提供 onChange
<input value={value} /> // 无法修改!

// ✅ 正确:提供 onChange
<input value={value} onChange={e => setValue(e.target.value)} />

8. 模式选择指北

graph TD
    A[需要复用逻辑] --> B{是否需要状态?}
    B -->|是| C[自定义 Hook]
    B -->|否| D[纯函数]
    A --> E[需要修改渲染逻辑]
    E --> F[Render Props]
    E --> G[组件复合]
    A --> H[需要添加横切关注点]
    H --> I[HOC 或 Hook]

9. 总结

React 设计模式的选择建议:

  • 优先使用 Hooks: 自定义 Hook 是 React 16.8+ 推荐的逻辑复用方式
  • 组件复合优先: 优先通过组合而非继承构建组件
  • 按需选择: 根据实际场景选择合适的模式,不要过度设计

相关阅读