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

前端徐徐
+关注
目录
打赏
0
0
0
0
39
分享
相关文章
如何使用组合组件和高阶组件实现复杂的 React 应用程序?
如何使用组合组件和高阶组件实现复杂的 React 应用程序?
166 68
在 React 中,组合组件和高阶组件在性能方面有何区别?
在 React 中,组合组件和高阶组件在性能方面有何区别?
147 67
除了高阶组件和render props,还有哪些在 React 中实现代码复用的方法?
除了高阶组件和render props,还有哪些在 React 中实现代码复用的方法?
153 62
React 中高阶组件的原理是什么?
React 中高阶组件的原理是什么?
147 57
在 React 中使用高阶组件时,如何避免命名冲突?
在 React 中使用高阶组件时,如何避免命名冲突?
125 56
React音频播放列表组件:常见问题、易错点与解决方案
本文介绍了在React中实现音频播放列表时常见的挑战及解决方案。通过基础实现、常见问题分析和最佳实践,帮助开发者避免状态管理、生命周期控制和事件处理中的陷阱。关键点包括使用`useRef`操作音频元素、`useState`同步播放状态、全局状态管理防止多音频同时播放、以及通过`useEffect`清理资源。还提供了代码示例和跨浏览器兼容性处理方法,确保高效实现功能并减少调试时间。
161 30
React 音频音量控制组件 Audio Volume Control
在现代Web应用中,音频播放功能不可或缺。React以其声明式编程和组件化开发模式,非常适合构建复杂的音频音量控制组件。本文介绍了如何使用HTML5 `&lt;audio&gt;`元素与React结合,实现直观的音量控制系统,并解决了常见问题如音量范围不合理、初始音量设置及性能优化等,帮助开发者打造优秀的音频播放器。
148 27
React 图片组件样式自定义:常见问题与解决方案
在 React 开发中,图片组件的样式自定义常因细节问题导致布局错乱、性能损耗或交互异常。本文系统梳理常见问题及解决方案,涵盖基础样式应用、响应式设计、加载状态与性能优化等,结合代码案例帮助开发者高效实现图片组件的样式控制。重点解决图片尺寸不匹配、边框阴影不一致、移动端显示模糊、加载失败处理及懒加载等问题,并总结易错点和最佳实践,助力开发者提升开发效率和用户体验。
138 22
React 音频播放控制组件 Audio Controls
本文介绍了如何使用React构建音频播放控制组件,涵盖HTML5 `&lt;audio&gt;`标签和React组件化思想的基础知识。针对常见问题如播放状态管理、进度条更新不准确及跨浏览器兼容性,提供了详细的解决方案和代码示例。同时,还总结了易错点及避免方法,如确保音频加载完成再操作、处理音频错误等,帮助开发者实现稳定且功能强大的音频播放器。
162 11
React 音频进度条组件 Audio Progress Bar
在现代Web开发中,音频播放功能不可或缺。使用React构建音频进度条组件,不仅能实现播放控制和拖动跳转,还能确保代码的可维护性和复用性。本文介绍了如何利用HTML5 `&lt;audio&gt;`标签的基础功能、解决获取音频时长和当前时间的问题、动态更新进度条样式,并避免常见错误如忘记移除事件监听器和忽略跨浏览器兼容性。通过这些方法,开发者可以打造高质量的音频播放器,提升用户体验。
118 6
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等

登录插画

登录以查看您的控制台资源

管理云资源
状态一览
快捷访问