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组件系统。每种模式都有其适用场景,理解它们的优缺点能帮助你在实际开发中做出最佳选择。

相关推荐
浩星3 小时前
css实现类似element官网的磨砂屏幕效果
前端·javascript·css
一只小风华~3 小时前
Vue.js 核心知识点全面解析
前端·javascript·vue.js
2022.11.7始学前端3 小时前
n8n第七节 只提醒重要的待办
前端·javascript·ui·n8n
SakuraOnTheWay3 小时前
React Grab实践 | 记一次与Cursor的有趣对话
前端·cursor
阿星AI工作室4 小时前
gemini3手势互动圣诞树保姆级教程来了!附提示词
前端·人工智能
徐小夕4 小时前
知识库创业复盘:从闭源到开源,这3个教训价值百万
前端·javascript·github
xhxxx4 小时前
函数执行完就销毁?那闭包里的变量凭什么活下来!—— 深入 JS 内存模型
前端·javascript·ecmascript 6
StarkCoder4 小时前
求求你试试 DiffableDataSource!别再手算 indexPath 了(否则迟早崩)
前端
fxshy4 小时前
Cursor 前端Global Cursor Rules
前端·cursor
红彤彤4 小时前
前端接入sse(EventSource)(@fortaine/fetch-event-source)
前端