React18+快速入门 - 4.组件插槽

在React中,"插槽"概念不像Vue那样有专门的语法,但可以通过多种方式实现类似功能。下面我为你详细介绍React中实现组件插槽的各种方法。

1. 基本概念:什么是组件插槽

组件插槽是一种允许父组件向子组件传递可复用的UI片段的技术。它使得组件更加灵活和可重用。

2. props.children:最简单的插槽

props.children 是React内置的插槽机制,可以接收任意JSX内容。

基本用法

jsx 复制代码
// Card.jsx - 子组件
const Card = ({ title, children }) => {
  return (
    <div className="card">
      {title && <h3 className="card-title">{title}</h3>}
      <div className="card-content">
        {children} {/* 这里是插槽位置 */}
      </div>
    </div>
  );
};

// App.jsx - 父组件
const App = () => {
  return (
    <Card title="用户信息">
      {/* 这里的JSX会被传递到Card的children中 */}
      <p>用户名:张三</p>
      <p>邮箱:zhangsan@example.com</p>
    </Card>
  );
};

处理多个子元素

jsx 复制代码
const TabList = ({ children }) => {
  return (
    <div className="tab-list">
      {React.Children.map(children, (child, index) => {
        // 可以为每个子元素添加额外的props
        return React.cloneElement(child, {
          index,
          key: `tab-${index}`,
        });
      })}
    </div>
  );
};

3. 命名插槽:使用props传递

当需要多个插槽时,可以使用props传递特定的JSX。

简单命名插槽

jsx 复制代码
// Layout.jsx
const Layout = ({ header, sidebar, content, footer }) => {
  return (
    <div className="layout">
      <header className="layout-header">{header}</header>
      <div className="layout-body">
        <aside className="layout-sidebar">{sidebar}</aside>
        <main className="layout-content">{content}</main>
      </div>
      <footer className="layout-footer">{footer}</footer>
    </div>
  );
};

// App.jsx
const App = () => {
  return (
    <Layout
      header={<h1>我的网站</h1>}
      sidebar={
        <nav>
          <ul>
            <li>首页</li>
            <li>关于</li>
            <li>联系</li>
          </ul>
        </nav>
      }
      content={
        <article>
          <h2>文章标题</h2>
          <p>文章内容...</p>
        </article>
      }
      footer={<p>© 2024 版权所有</p>}
    />
  );
};

使用对象传递多个插槽

jsx 复制代码
// Modal.jsx
const Modal = ({ slots, isOpen, onClose }) => {
  if (!isOpen) return null;
  
  return (
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal" onClick={(e) => e.stopPropagation()}>
        <div className="modal-header">
          {slots.header || <h3>默认标题</h3>}
          <button onClick={onClose}>×</button>
        </div>
        <div className="modal-body">{slots.body}</div>
        <div className="modal-footer">{slots.footer}</div>
      </div>
    </div>
  );
};

// 使用
const App = () => {
  const [isModalOpen, setIsModalOpen] = useState(false);
  
  return (
    <div>
      <button onClick={() => setIsModalOpen(true)}>打开模态框</button>
      <Modal
        isOpen={isModalOpen}
        onClose={() => setIsModalOpen(false)}
        slots={{
          header: <h3>自定义标题</h3>,
          body: <p>这是模态框的内容...</p>,
          footer: (
            <>
              <button onClick={() => setIsModalOpen(false)}>取消</button>
              <button onClick={() => alert('确认!')}>确认</button>
            </>
          ),
        }}
      />
    </div>
  );
};

4. Render Props模式

Render Props是一种通过函数prop共享组件逻辑的模式。

jsx 复制代码
// MouseTracker.jsx
class MouseTracker extends React.Component {
  state = { x: 0, y: 0 };

  handleMouseMove = (event) => {
    this.setState({
      x: event.clientX,
      y: event.clientY,
    });
  };

  render() {
    return (
      <div onMouseMove={this.handleMouseMove}>
        {/* 调用render函数,将状态作为参数传递 */}
        {this.props.render(this.state)}
      </div>
    );
  }
}

// 使用 - 方式1:render prop
const App = () => {
  return (
    <MouseTracker
      render={({ x, y }) => (
        <div>
          <h1>移动鼠标!</h1>
          <p>
            当前鼠标位置: ({x}, {y})
          </p>
        </div>
      )}
    />
  );
};

// 使用 - 方式2:children作为函数
const MouseTrackerWithChildren = ({ children }) => {
  // ...相同逻辑
  return (
    <div onMouseMove={this.handleMouseMove}>
      {children(this.state)}
    </div>
  );
};

// 使用
const App = () => {
  return (
    <MouseTrackerWithChildren>
      {({ x, y }) => (
        <p>鼠标位置: ({x}, {y})</p>
      )}
    </MouseTrackerWithChildren>
  );
};

5. 函数作为子组件

这是Render Props的变体,使用children作为函数。

jsx 复制代码
// DataFetcher.jsx
const DataFetcher = ({ url, children }) => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

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

  // children作为函数被调用
  return children({ data, loading, error });
};

// 使用
const App = () => {
  return (
    <DataFetcher url="https://api.example.com/users">
      {({ data, loading, error }) => {
        if (loading) return <div>加载中...</div>;
        if (error) return <div>错误: {error.message}</div>;
        return (
          <ul>
            {data?.map((user) => (
              <li key={user.id}>{user.name}</li>
            ))}
          </ul>
        );
      }}
    </DataFetcher>
  );
};

6. 组合组件模式

通过组合多个专门组件来创建复杂UI。

jsx 复制代码
// Accordion.jsx - 主组件
const Accordion = ({ children }) => {
  return <div className="accordion">{children}</div>;
};

// Accordion.Item - 子项组件
const AccordionItem = ({ title, children, defaultOpen = false }) => {
  const [isOpen, setIsOpen] = useState(defaultOpen);
  
  return (
    <div className="accordion-item">
      <button 
        className="accordion-header" 
        onClick={() => setIsOpen(!isOpen)}
      >
        {title}
        <span>{isOpen ? '▲' : '▼'}</span>
      </button>
      {isOpen && <div className="accordion-content">{children}</div>}
    </div>
  );
};

// 将Item作为Accordion的静态属性
Accordion.Item = AccordionItem;

// 使用
const App = () => {
  return (
    <Accordion>
      <Accordion.Item title="第一章:React基础">
        <p>React是一个用于构建用户界面的JavaScript库...</p>
      </Accordion.Item>
      <Accordion.Item title="第二章:组件与Props">
        <p>组件允许你将UI拆分为独立的、可复用的部分...</p>
      </Accordion.Item>
      <Accordion.Item title="第三章:状态与生命周期">
        <p>State是组件的内部数据存储...</p>
      </Accordion.Item>
    </Accordion>
  );
};

更复杂的组合组件示例

jsx 复制代码
// Table组件组合示例
const Table = ({ children }) => {
  return <table className="table">{children}</table>;
};

const TableHeader = ({ children }) => {
  return <thead className="table-header">{children}</thead>;
};

const TableBody = ({ children }) => {
  return <tbody className="table-body">{children}</tbody>;
};

const TableRow = ({ children }) => {
  return <tr className="table-row">{children}</tr>;
};

const TableCell = ({ children, header = false }) => {
  const Tag = header ? 'th' : 'td';
  return <Tag className="table-cell">{children}</Tag>;
};

// 组合所有子组件
Table.Header = TableHeader;
Table.Body = TableBody;
Table.Row = TableRow;
Table.Cell = TableCell;

// 使用
const UserTable = ({ users }) => {
  return (
    <Table>
      <Table.Header>
        <Table.Row>
          <Table.Cell header>ID</Table.Cell>
          <Table.Cell header>姓名</Table.Cell>
          <Table.Cell header>邮箱</Table.Cell>
        </Table.Row>
      </Table.Header>
      <Table.Body>
        {users.map((user) => (
          <Table.Row key={user.id}>
            <Table.Cell>{user.id}</Table.Cell>
            <Table.Cell>{user.name}</Table.Cell>
            <Table.Cell>{user.email}</Table.Cell>
          </Table.Row>
        ))}
      </Table.Body>
    </Table>
  );
};

7. 高阶组件(HOC)中的插槽

高阶组件可以包装组件并注入额外的功能。

jsx 复制代码
// withAuth高阶组件
const withAuth = (WrappedComponent) => {
  return (props) => {
    const [isAuthenticated, setIsAuthenticated] = useState(false);
    const [user, setUser] = useState(null);
    
    // 模拟认证逻辑
    useEffect(() => {
      // 检查本地存储或调用API
      const token = localStorage.getItem('token');
      if (token) {
        setIsAuthenticated(true);
        // 获取用户信息
        setUser({ name: '张三', role: 'admin' });
      }
    }, []);
    
    // 登录函数
    const login = (credentials) => {
      // 登录逻辑
      localStorage.setItem('token', 'fake-token');
      setIsAuthenticated(true);
      setUser({ name: '张三', role: 'admin' });
    };
    
    // 登出函数
    const logout = () => {
      localStorage.removeItem('token');
      setIsAuthenticated(false);
      setUser(null);
    };
    
    if (!isAuthenticated) {
      return (
        <div>
          <h3>请先登录</h3>
          <button onClick={() => login({})}>模拟登录</button>
        </div>
      );
    }
    
    // 将认证相关的props传递给包装的组件
    return (
      <WrappedComponent
        {...props}
        user={user}
        logout={logout}
      />
    );
  };
};

// 使用
const UserProfile = ({ user, logout }) => {
  return (
    <div>
      <h2>用户信息</h2>
      <p>姓名:{user.name}</p>
      <p>角色:{user.role}</p>
      <button onClick={logout}>退出登录</button>
    </div>
  );
};

const AuthenticatedUserProfile = withAuth(UserProfile);

// 在App中使用
const App = () => {
  return <AuthenticatedUserProfile />;
};

8. 总结与最佳实践

各种插槽模式的比较

模式 适用场景 优点 缺点
props.children 单一内容区域 简单直观,React内置支持 只能有一个插槽
命名props 多个固定插槽 灵活,可定义多个插槽 代码可能冗长
Render Props 逻辑复用 强大的逻辑共享能力 可能造成嵌套过深
函数作为children 动态内容 灵活,子组件控制渲染 需要理解函数作为子组件的概念
组合组件 复杂UI结构 清晰的结构,良好的封装性 需要设计良好的组件结构
HOC 横切关注点 逻辑复用,不侵入组件 可能造成props冲突,调试困难

最佳实践

  1. 简单场景用props.children :当只需要一个插槽时,优先使用props.children
  2. 多插槽用命名props:当有明确的多个插槽区域时,使用命名props
  3. 逻辑复用考虑Render Props:需要共享状态逻辑时使用
  4. 复杂UI用组合组件:构建复杂、可复用的UI组件时使用
  5. 保持一致性:在项目中统一插槽的使用模式
  6. 提供默认内容:为插槽提供合理的默认值或回退UI
  7. 文档化插槽:使用PropTypes或TypeScript明确插槽的接口

通过灵活运用这些模式,你可以构建出高度可复用、可维护的React组件系统。每种模式都有其适用场景,理解它们的优缺点能帮助你在实际开发中做出最佳选择。

相关推荐
zzz_23681 分钟前
个人 AI 记忆如何跨工具复用:用 Markdown、索引和 Skill 搭一个可治理的记忆库
前端·人工智能·react.js·前端框架·agent·agent测评
剑之所向2 小时前
.NET 官方`System.Threading.Channels`
前端·javascript·数据库
计算机魔术师2 小时前
GPT-6 Astra幻觉砍到2%,却被一种老招数轻松绕过
前端
cjy0001113 小时前
2026AI 产品如何从一次性 Demo 变成可重复使用的业务工具?
前端·人工智能·fde
IT_陈寒3 小时前
React的状态更新坑得我差点加班到天亮
前端·人工智能·后端
风骏时光牛马3 小时前
深挖底层逻辑:XX源码深度拆解分析
前端
渡我白衣3 小时前
并查集:基础认识与模拟实现
android·java·javascript·数据结构·c++·算法·并查集
电商API_180079052473 小时前
电商商品价格监控工具淘宝京东商品价格抓取API项目实操分享
java·大数据·开发语言·前端·数据挖掘
sxmas3 小时前
用状态机与DAG建模四阶段业务流程:以MIND模型为例
前端
八角丶4 小时前
Node 网络编程 —— TLS 模块
javascript·后端·node.js