前军教程网

中小站长与DIV+CSS网页布局开发技术人员的首选CSS学习平台

用React复合组件来解决在不同场景下复用组件的问题

你是否曾为创建需要适应不同场景的灵活React组件而苦恼?我发现了一种彻底改变我React开发方式的模式——复合组件(compound components)。

在本文中,我将通过实际可立即应用到项目中的示例,带你了解这个改变游戏规则的模式。

问题:相似组件,不同场景


让我们从一个常见场景开始:你有两个外观相似但存在于完全不同上下文中的UI元素。

想象一个联系人管理应用,你需要:

  1. 在专用页面上编辑联系人
  2. 在模态对话框中编辑联系人

这两个界面共享相同的表单字段、保存/取消按钮和标题——但它们的呈现方式不同:

  • 页面版:全屏布局,标题和按钮位于页眉
  • 模态版:紧凑对话框,有自己独特的样式约束

传统解决方案有两种:

  1. 创建两个逻辑重复的独立组件
  2. 创建一个根据上下文进行条件渲染的复杂组件

这两种方案都不理想。让我们看看复合组件如何优雅地解决这个问题。

如果你觉得这有帮助,请点个赞帮助更多人发现这篇指南!

解决方案:复合组件模式


复合组件是一种模式,你创建一个管理状态和行为的父组件,同时暴露用于渲染UI不同部分的子组件。

可以把它想象成HTML中的和——它们协同工作,但可以灵活排列。 下面是我们的实现方式:

// 在EditContactPage.jsx的用法
function EditContactPage() {
  return (
    <PageLayout>
      <EditContact.Root contactId={contactId}>
        <div className="right-section">
          <EditContact.Title />
          <EditContact.SubmitButtons />
        </div>
        <div className="content">
          <EditContact.FormInputs />
        </div>
      </EditContact.Root>
    </PageLayout>
  );
}

// 在ContactModal.jsx的用法

function ContactModal({ contactId, onClose }) {
  return (
    <EditContact.Root contactId={contactId}>
      <Modal 
        onClose={onClose}
        title={<EditContact.Title />}
        footer={<EditContact.SubmitButtons />}
      >
        <EditContact.FormInputs />
      </Modal>
    </EditContact.Root>
  );
}

看到这种简洁的设计了吗?两个组件使用相同的构建模块,但通过不同的排列方式来适配各自的特定布局。

构建复合组件


现在让我们看看如何实现这个模式。关键要素包括:

  • 用于共享状态的上下文
  • 管理逻辑的父组件
  • 使用该上下文的子组件

下面是具体实现:

import { createContext, useContext, useState, useEffect } from 'react';
// 创建context
const EditContactContext = createContext(null);
// 创建钩子来用context
function useEditContactContext() {
  const context = useContext(EditContactContext);
  if (!context) {
    throw new Error('EditContact subcomponents must be used within EditContact.Root');
  }
  return context;
}
// Root 组件来处理state和逻辑
function Root({ contactId, children }) {
  const [formState, setFormState] = useState({
    name: '',
    email: '',
    phone: ''
  });
  const [error, setError] = useState(null);
  const [isLoading, setIsLoading] = useState(false);
  
  // 获取联系detail
  const { data: contact } = useGetContact(contactId);
  
  // 保存联系信息
  const saveContact = useSaveContact({
    onSuccess: () => {
      // 成功状态下处理
    },
    onError: (err) => {
      setError(err.message);
    }
  });
  
  // 设置原始form数据
  useEffect(() => {
    if (contact) {
      setFormState({
        name: contact.name,
        email: contact.email,
        phone: contact.phone
      });
    }
  }, [contact]);
  
  // 处理提交的表单
  const handleSubmit = async () => {
    setIsLoading(true);
    try {
      await saveContact.mutateAsync({
        id: contactId,
        ...formState
      });
    } finally {
      setIsLoading(false);
    }
  };
  
  // context值
  const value = {
    contact,
    formState,
    setFormState,
    error,
    isLoading,
    handleSubmit
  };
  
  return (
    <EditContactContext.Provider value={value}>
      {children}
    </EditContactContext.Provider>
  );
}
// Title 组件
function Title() {
  const { contact } = useEditContactContext();
  return <>{contact ? `Edit ${contact.name}` : 'Create Contact'}</>;
}
// Submit buttons component
function SubmitButtons() {
  const { handleSubmit, isLoading } = useEditContactContext();
  return (
    <div className="button-group">
      <button 
        className="primary-button" 
        onClick={handleSubmit}
        disabled={isLoading}
      >
        {isLoading ? 'Saving...' : 'Save'}
      </button>
      <button className="secondary-button">Cancel</button>
    </div>
  );
}
// Form inputs 组件
function FormInputs() {
  const { formState, setFormState, error } = useEditContactContext();
  
  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormState(prev => ({
      ...prev,
      [name]: value
    }));
  };
  
  return (
    <form className="contact-form">
      {error && <div className="error-message">{error}</div>}
      <div className="form-group">
        <label htmlFor="name">Name</label>
        <input
          id="name"
          name="name"
          value={formState.name}
          onChange={handleChange}
        />
      </div>
      <div className="form-group">
        <label htmlFor="email">Email</label>
        <input
          id="email"
          name="email"
          type="email"
          value={formState.email}
          onChange={handleChange}
        />
      </div>
      <div className="form-group">
        <label htmlFor="phone">Phone</label>
        <input
          id="phone"
          name="phone"
          value={formState.phone}
          onChange={handleChange}
        />
      </div>
    </form>
  );
}
// 导出这些组件
const EditContact = {
  Root,
  Title,
  SubmitButtons,
  FormInputs
};
export default EditContact;

为什么这个模式具有革命性?

当我开始使用复合组件后,才真正体会到它的强大之处:

灵活性:你可以将子组件放置在组件树的任何位置
关注点分离:逻辑集中管理,UI组合独立实现
可复用性:相同组件可应用于完全不同的场景
可读性:JSX代码清晰展现UI结构
可维护性:核心逻辑的变更只需在一个地方进行

现代UI库如Chakra UI、shadcn/ui和Radix UI广泛采用这个模式绝非偶然——它能创建既强大又灵活的组件。

何时使用复合组件?

当遇到以下情况时,复合组件特别有用:

  • 组件需要在不同上下文中呈现不同布局
  • 需要在多个UI元素间共享复杂状态
  • 希望开发者能灵活组合你的组件
  • 正在构建组件库或设计系统

核心要点:


复合组件让你能创建灵活可组合的UI元素
使用React Context在父子组件间共享状态
该模式实现了逻辑与表现的分离
特别适合需要适应不同布局的场景

下次当你需要构建具有多种变体的复杂组件时,不妨尝试复合组件模式。它可能会像改变我一样,彻底改变你对React组件的认知方式。

哪些React模式改变了你的开发流程?欢迎在评论区分享!


发表评论:

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言