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 中传递组件作为属性,并帮助你选择适合你的最佳实现方式。

相关文章
|
19天前
|
前端开发 JavaScript 测试技术
React 分页组件 Pagination
本文介绍了如何在 React 中从零构建分页组件,涵盖基础概念、常见问题及解决方案。通过示例代码详细讲解了分页按钮的创建、分页按钮过多、初始加载慢、状态管理混乱等常见问题的解决方法,以及如何避免边界条件、性能优化和用户反馈等方面的易错点。旨在帮助开发者更好地理解和掌握 React 分页组件的开发技巧,提升应用的性能和用户体验。
54 0
|
23天前
|
移动开发 前端开发 API
React 拖拽组件 Drag & Drop
本文介绍了在 React 中实现拖拽功能的方法,包括使用原生 HTML5 Drag and Drop API 和第三方库 `react-dnd`。通过代码示例详细讲解了基本的拖拽实现、常见问题及易错点,帮助开发者更好地理解和应用拖拽功能。
56 9
|
18天前
|
前端开发 UED 开发者
React 分页组件 Pagination
本文介绍了如何在 React 中实现分页组件,从基础概念到常见问题及解决方案。分页组件用于将大量数据分成多个页面,提升用户体验。文章详细讲解了分页组件的基本结构、快速入门步骤、以及如何处理页面跳转不平滑、页码过多导致布局混乱、边界条件处理和数据加载延迟等问题。通过本文,读者可以全面了解并掌握 React 分页组件的开发技巧。
29 2
|
22天前
|
设计模式 前端开发 编译器
与普通组件相比,React 泛型组件有哪些优势?
与普通组件相比,React 泛型组件有哪些优势?
33 6
|
1月前
|
前端开发 JavaScript 安全
学习如何为 React 组件编写测试:
学习如何为 React 组件编写测试:
37 2
|
2月前
|
前端开发 JavaScript 测试技术
React 高阶组件 (HOC) 应用
【10月更文挑战第16天】高阶组件(HOC)是 React 中一种复用组件逻辑的方式,通过接受一个组件并返回新组件来实现。本文介绍了 HOC 的基础概念、核心功能和常见问题,包括静态方法丢失、ref 丢失、多个 HOC 组合和 props 冲突的解决方案,并提供了具体的 React 代码示例。通过本文,读者可以更好地理解和应用 HOC,提高代码的复用性和可维护性。
66 8
|
2月前
|
缓存 前端开发 JavaScript
前端serverless探索之组件单独部署时,利用rxjs实现业务状态与vue-react-angular等框架的响应式状态映射
本文深入探讨了如何将RxJS与Vue、React、Angular三大前端框架进行集成,通过抽象出辅助方法`useRx`和`pushPipe`,实现跨框架的状态管理。具体介绍了各框架的响应式机制,展示了如何将RxJS的Observable对象转化为框架的响应式数据,并通过示例代码演示了使用方法。此外,还讨论了全局状态源与WebComponent的部署优化,以及一些实践中的改进点。这些方法不仅简化了异步编程,还提升了代码的可读性和可维护性。
|
24天前
|
前端开发 UED
React 模态框 Modal 组件详解
【10月更文挑战第27天】本文介绍了如何在 React 中实现一个功能完善的模态框组件。从基础概念入手,逐步讲解了简单的模态框实现、CSS 样式、传递子组件、键盘事件处理等高级功能。同时,还探讨了常见问题及易错点,如背景点击关闭、键盘事件冲突和动画效果。通过本文,读者可以全面了解 React 模态框组件的实现细节。
45 0
|
2月前
|
前端开发 JavaScript 调度
React 组件状态(State)
10月更文挑战第8天
33 1
|
24天前
|
前端开发 JavaScript 开发者
颠覆传统:React框架如何引领前端开发的革命性变革
【10月更文挑战第32天】本文以问答形式探讨了React框架的特性和应用。React是一款由Facebook推出的JavaScript库,以其虚拟DOM机制和组件化设计,成为构建高性能单页面应用的理想选择。文章介绍了如何开始一个React项目、组件化思想的体现、性能优化方法、表单处理及路由实现等内容,帮助开发者更好地理解和使用React。
62 9