React将组件作为属性传递的最佳实践

简介: 本文探讨了在React中将组件作为属性传递的三种常见方式:作为元素传递、作为组件传递、作为函数传递。通过构建带图标的按钮组件,对比分析了每种方式的优缺点,最终推荐将组件作为函数传递,因为它提供了更好的可控性、灵活性和可扩展性。

本文首发微信公众号:前端徐徐。

前言

在 React 中,一件事往往有上百万种不同的实现方式。如果需要将一个组件作为属性传递给另一个组件,我们该怎么做呢?如果我们在一些流行的开源库中寻找答案,会发现:

  • 我们可以像 Material UI 库在按钮的 startIcon 属性中那样,把它们作为元素传递。
  • 我们可以像 react-select 库对其 components 属性那样,把它们作为组件本身传递。
  • 我们可以像 Material UI Data Grid 组件对其 renderCell 属性那样,把它们作为函数传递。

一点都不混乱 😅

那么哪种方式是最好的,哪种方式应该避免呢?哪种方式应该被列入“React 最佳实践”列表中,为什么呢?让我们一起来探讨一下吧!

或者,如果你喜欢剧透,可以直接滚动到文章的总结部分。那里有这些问题的明确答案 😉

为什么我们需要将组件作为属性传递

在开始编写代码之前,我们先来理解一下为什么我们需要将组件作为属性传递。简短的回答是:为了灵活性以及简化组件之间的数据共享。

假设我们在实现一个带图标的按钮。我们当然可以这样实现:

const Button = ({ children }: { children: ReactNode }) => {
  return (
    <button>
      <SomeIcon size="small" color="red" />
      {children}
    </button>
  );
};

但是,如果我们需要让用户更换图标呢?我们可以为此引入一个 iconName 属性:

type Icons = 'cross' | 'warning' | ... // 所有支持的图标
const getIconFromName = (iconName: Icons) => {
  switch (iconName) {
    case 'cross':
      return <CrossIcon size="small" color="red" />;
    ...
    // 所有其他支持的图标
  }
}
const Button = ({ children, iconName }: { children: ReactNode, iconName: Icons }) => {
  const icon = getIconFromName(name);
  return <button>
    {icon}
    {children}
  </button>
}

如果需要让用户更改图标的外观呢?例如更改其大小和颜色?我们还需要为此引入一些属性:

type Icons = 'cross' | 'warning' | ... // 所有支持的图标
type IconProps = {
  size: 'small' | 'medium' | 'large',
  color: string
};
const getIconFromName = (iconName: Icons, iconProps: IconProps) => {
  switch (iconName) {
    case 'cross':
      return <CrossIcon {...iconProps} />;
    ...
    // 所有其他支持的图标
  }
}
const Button = ({ children, iconName, iconProps }: { children: ReactNode, iconName: Icons, iconProps: IconProps }) => {
  const icon = getIconFromName(name, iconProps);
  return <button>
    {icon}
    {children}
  </button>
}

如果用户希望在按钮发生变化时更改图标,例如当按钮被悬停时更改图标的颜色。为了实现这个功能,我们需要暴露 onHover 回调,在每个父组件中引入状态管理,在按钮被悬停时设置状态等等。

这不仅是一个非常有限和复杂的 API。我们还强制我们的 Button 组件知道它可以渲染的每一个图标,这意味着这个 Button 的捆绑 JS 不仅包括它自己的代码,还包括列表上的每一个图标。这将是一个非常重的按钮 🙂

这时候将组件作为属性传递的方式就派上用场了。我们无需将图标的详细描述(如名称和属性)传递给 Button,Button 可以直接说:“给我一个图标,我不在乎是哪一个,由你选择,我会在合适的位置渲染它”。

让我们看看可以通过三种模式实现这一点的方法:

  • 作为元素传递
  • 作为组件传递
  • 作为函数传递

构建带图标的按钮

准确地说,我们构建三个按钮,分别使用三种不同的 API 来传递图标,然后进行比较。希望最后能明显看出哪种方式更好。

我们将使用 material UI 组件库中的一个图标作为例子。让我们从基础开始,先构建 API。

第一个:作为 React 元素传递图标

我们只需要将一个元素传递给按钮的 icon 属性,然后像其他元素一样渲染它。

type ButtonProps = {
  children: ReactNode;
  icon: ReactElement<IconProps>;
};
export const ButtonWithIconElement = ({ children, icon }: ButtonProps) => {
  return (
    <button>
      {icon}
      {children}
    </button>
  );
};

然后可以像这样使用它:

<ButtonWithIconElement icon={<AccessAlarmIconGoogle />}>button here</ButtonWithIconElement>

第二个:作为组件传递图标

我们需要创建一个以大写字母开头的属性来表示它是一个组件,然后像其他组件一样从属性中渲染该组件。

type ButtonProps = {
  children: ReactNode;
  Icon: ComponentType<IconProps>;
};
export const ButtonWithIconComponent = ({ children, Icon }: ButtonProps) => {
  return (
    <button>
      <Icon />
      {children}
    </button>
  );
};

然后可以像这样使用它:

import AccessAlarmIconGoogle from '@mui/icons-material/AccessAlarm';
<ButtonWithIconComponent Icon={AccessAlarmIconGoogle}>button here</ButtonWithIconComponent>;

第三个:作为函数传递图标

我们需要创建一个以 render 开头的属性来表示它是一个渲染函数,即一个返回元素的函数,调用该函数并将结果添加到组件的渲染函数中。

type ButtonProps = {
  children: ReactNode;
  renderIcon: () => ReactElement<IconProps>;
};
export const ButtonWithIconRenderFunc = ({ children, renderIcon }: ButtonProps) => {
  const icon = renderIcon();
  return (
    <button>
      {icon}
      {children}
    </button>
  );
};

然后可以像这样使用它:

<ButtonWithIconRenderFunc renderIcon={() => <AccessAlarmIconGoogle />}>button here</ButtonWithIconRenderFunc>

这很容易!现在我们的按钮可以在那个特殊的图标插槽中渲染任何图标,而不需要知道是什么。请参阅代码示例中的实际工作示例。

调整图标的大小和颜色

首先看看我们是否可以根据需要调整图标,而不会影响按钮。毕竟,这些模式的主要承诺不就是为了这个吗?

第一个:作为 React 元素传递图标

非常简单:我们只需要将一些属性传递给图标。我们使用 material UI 图标,它们提供 fontSizecolor 属性。

<ButtonWithIconElement icon={<AccessAlarmIconGoogle fontSize="small" color="warning" />}>button here</ButtonWithIconElement>

第二个:作为组件传递图标

同样简单:我们需要将图标提取为一个组件,并在返回元素中传递属性。

const AccessAlarmIcon = () => <AccessAlarmIconGoogle fontSize="small" color="error" />;
const Page = () => {
  return <ButtonWithIconComponent Icon={AccessAlarmIcon}>button here</ButtonWithIconComponent>;
};

重要提示:AccessAlarmIcon 组件应始终定义在 Page 组件之外,否则每次 Page 重新渲染时都会重新创建该组件,这对性能非常不利且容易出现错误。如果你不熟悉这种情况,这篇文章可以帮助你了解如何编写高性能的 React 代码:如何编写高性能的 React 代码:规则、模式、注意事项和禁忌。

第三个:作为函数传递图标

几乎和第一个一样:只需将属性传递给元素。

<ButtonWithIconRenderFunc
  renderIcon={() => (
    <AccessAlarmIconGoogle fontSize="small" color="success" />
  )}
>
button here
</ButtonWithIconRenderFunc>

所有三种方式都轻松完成,我们可以无限制地修改图标,而不需要触碰按钮。相比于最初的 iconNameiconProps,这种方式更具灵活性。

按钮默认图标大小

你可能已经注意到,我在所有三个示例中使用了相同的图标大小。当实现一个通用的按钮组件时,更可能会有一些属性来控制按钮的大小。无限制的灵活性是好的,但对于设计系统来说,你会希望有一些预定义的按钮类型。而对于不同大小的按钮,你希望按钮来控制图标的大小,而不是由消费者来决定,以免不小心把小图标放在大按钮中或反之亦然。

现在变得有趣了:是否可以让按钮控制图标的某一方面,同时保持灵活性?

第一个:作为React 元素传递图标

这里我们会遇到麻烦。考虑一下 ButtonWithIconElement

type ButtonProps = {
  children: ReactNode;
  icon: ReactElement<IconProps>;
  size: 'small' | 'medium' | 'large';
};
const ButtonWithIconElement = ({ children, icon, size }: ButtonProps) => {
  return (
    <button className={`btn-${size}`}>
      {icon}
      {children}
    </button>
  );
};

icon 是一个 React 元素。fontSize 属性已经嵌入其中。我们的按钮组件无法覆盖它。可能的解决方案是创建图标的克隆:

import { cloneElement } from 'react';
type ButtonProps = {
  children: ReactNode;
  icon: ReactElement<IconProps>;
  size: 'small' | 'medium' | 'large';
};
const ButtonWithIconElement = ({ children, icon, size }: ButtonProps) => {
  const newIcon = cloneElement(icon, { fontSize: size });
  return (
    <button className={`btn-${size}`}>
      {newIcon}
      {children}
    </button>
  );
};

非常简单有效,但有时会带来不必要的复杂性。此实现允许将 fontSize 覆盖成另一个值:

<ButtonWithIconElement icon={<AccessAlarmIconGoogle fontSize="small" />} size="large">button here</ButtonWithIconElement>

第二个:作为组件传递图标

我们有更多的灵活性:我们可以将属性传递给组件:

type ButtonProps = {
  children: ReactNode;
  Icon: ComponentType<IconProps>;
  size: 'small' | 'medium' | 'large';
};
const ButtonWithIconComponent = ({ children, Icon, size }: ButtonProps) => {
  return (
    <button className={`btn-${size}`}>
      <Icon fontSize={size} />
      {children}
    </button>
  );
};

然后在 Page 中渲染:

const Page = () => {
  return <ButtonWithIconComponent Icon={AccessAlarmIconGoogle} size="large">button here</ButtonWithIconComponent>;
};

这也有效:AccessAlarmIconGoogle 获取到 fontSize 属性并根据需要渲染。我们保留了覆盖的能力:按钮不在乎如何设置图标大小。

第三个:作为函数传递图标

这同样简单:只需将属性传递给 renderIcon 函数。

type ButtonProps = {
  children: ReactNode;
  renderIcon: (props: IconProps) => ReactElement;
  size: 'small' | 'medium' | 'large';
};
const ButtonWithIconRenderFunc = ({ children, renderIcon, size }: ButtonProps) => {
  const icon = renderIcon({ fontSize: size });
  return (
    <button className={`btn-${size}`}>
      {icon}
      {children}
    </button>
  );
};

然后渲染:

<ButtonWithIconRenderFunc renderIcon={props => <AccessAlarmIconGoogle {...props} />} size="large">button here</ButtonWithIconRenderFunc>

你选择哪种方式?

这三个模式都有效,展示了我们想要的灵活性。但是,是否有更好的选择呢?

如果是我,我会选择将组件作为函数传递。原因有几个:

  • 更好的可控性:我们可以覆盖任何属性,无需担心实现的复杂性。
  • 更好的灵活性:允许传递除图标外的任何属性,而不需要担心属性被覆盖。
  • 更好的可扩展性:我们可以轻松添加新的属性和方法,而无需修改现有代码。

最后,希望这些模式能够帮助你更好地理解如何在 React 中传递组件作为属性,并帮助你选择适合你的最佳实现方式。

相关文章
|
1天前
|
编解码 Java 程序员
写代码还有专业的编程显示器?
写代码已经十个年头了, 一直都是习惯直接用一台Mac电脑写代码 偶尔接一个显示器, 但是可能因为公司配的显示器不怎么样, 还要接转接头 搞得桌面杂乱无章,分辨率也低,感觉屏幕还是Mac自带的看着舒服
|
3天前
|
存储 缓存 关系型数据库
MySQL事务日志-Redo Log工作原理分析
事务的隔离性和原子性分别通过锁和事务日志实现,而持久性则依赖于事务日志中的`Redo Log`。在MySQL中,`Redo Log`确保已提交事务的数据能持久保存,即使系统崩溃也能通过重做日志恢复数据。其工作原理是记录数据在内存中的更改,待事务提交时写入磁盘。此外,`Redo Log`采用简单的物理日志格式和高效的顺序IO,确保快速提交。通过不同的落盘策略,可在性能和安全性之间做出权衡。
1540 5
|
1月前
|
弹性计算 人工智能 架构师
阿里云携手Altair共拓云上工业仿真新机遇
2024年9月12日,「2024 Altair 技术大会杭州站」成功召开,阿里云弹性计算产品运营与生态负责人何川,与Altair中国技术总监赵阳在会上联合发布了最新的“云上CAE一体机”。
阿里云携手Altair共拓云上工业仿真新机遇
|
7天前
|
人工智能 Rust Java
10月更文挑战赛火热启动,坚持热爱坚持创作!
开发者社区10月更文挑战,寻找热爱技术内容创作的你,欢迎来创作!
577 22
|
3天前
|
存储 SQL 关系型数据库
彻底搞懂InnoDB的MVCC多版本并发控制
本文详细介绍了InnoDB存储引擎中的两种并发控制方法:MVCC(多版本并发控制)和LBCC(基于锁的并发控制)。MVCC通过记录版本信息和使用快照读取机制,实现了高并发下的读写操作,而LBCC则通过加锁机制控制并发访问。文章深入探讨了MVCC的工作原理,包括插入、删除、修改流程及查询过程中的快照读取机制。通过多个案例演示了不同隔离级别下MVCC的具体表现,并解释了事务ID的分配和管理方式。最后,对比了四种隔离级别的性能特点,帮助读者理解如何根据具体需求选择合适的隔离级别以优化数据库性能。
201 3
|
10天前
|
JSON 自然语言处理 数据管理
阿里云百炼产品月刊【2024年9月】
阿里云百炼产品月刊【2024年9月】,涵盖本月产品和功能发布、活动,应用实践等内容,帮助您快速了解阿里云百炼产品的最新动态。
阿里云百炼产品月刊【2024年9月】
|
10天前
|
Linux 虚拟化 开发者
一键将CentOs的yum源更换为国内阿里yum源
一键将CentOs的yum源更换为国内阿里yum源
571 5
|
23天前
|
存储 关系型数据库 分布式数据库
GraphRAG:基于PolarDB+通义千问+LangChain的知识图谱+大模型最佳实践
本文介绍了如何使用PolarDB、通义千问和LangChain搭建GraphRAG系统,结合知识图谱和向量检索提升问答质量。通过实例展示了单独使用向量检索和图检索的局限性,并通过图+向量联合搜索增强了问答准确性。PolarDB支持AGE图引擎和pgvector插件,实现图数据和向量数据的统一存储与检索,提升了RAG系统的性能和效果。
|
6天前
|
XML 安全 Java
【Maven】依赖管理,Maven仓库,Maven核心功能
【Maven】依赖管理,Maven仓库,Maven核心功能
233 3
|
9天前
|
存储 人工智能 搜索推荐
数据治理,是时候打破刻板印象了
瓴羊智能数据建设与治理产品Datapin全面升级,可演进扩展的数据架构体系为企业数据治理预留发展空间,推出敏捷版用以解决企业数据量不大但需构建数据的场景问题,基于大模型打造的DataAgent更是为企业用好数据资产提供了便利。
327 2