明智地设计你的 React 组件

2025-05-27

明智地设计你的 React 组件

欢迎阅读这篇文章,我将与你分享我在组件开发方面的思考过程。我将把一个组件提取成模块化的部分,解释每个部分存在的原因,以及它们最终如何组合在一起,形成一个既可靠又灵活、易于维护的结果。

关注点分离(SoC)

编程,尤其是组件开发,最重要的方面之一是“关注点分离”(SoC)。这种设计理念可以省去日后的很多麻烦,并且适用于你可能面临的任何开发挑战。SoC 的本质是每个组件都有各自的职责,并且不会“泄露”给其他组件。

对于我们这些 FED 来说,在创建组件时这一点尤为明显。拥有一个好的 SoC 意味着我们可以轻松地移动、扩展和复用组件。但是,仅仅了解组件的外观和行为就足以让我们直接开始编写代码了吗?我们如何知道我们的组件是否拥有一个好的 SoC?

我希望我将在这里与您分享的这个例子能够让事情变得更加清晰,并帮助您更好地制作组件。

注意:虽然本文以 React 为背景,但大多数概念并不局限于 React,也可以在其他框架中实践。


要求

我们的组件乍一看相当简单。我们有一些可交换的内容,可以使用箭头或点击特定页面索引进行分页。
以下是它的粗略线框图,可以帮助您想象它应该是什么样子:

图片描述

等等,让我们来加点料——
页面之间应该支持三种过渡效果:淡入淡出、滑动和翻转。另一方面,分页功能应该支持只显示箭头、只显示带编号的项目符号,或者根本不显示。
整个页面还应该支持自动分页,页面会自动切换。
哦,对了,如果启用了自动分页,鼠标悬停在页面上会暂停过渡。

让它静置一分钟,然后我们走吧:)

最简单的方法是将所有内容放在同一个组件中,即包含页面和分页的单个文件中,但我们知道产品需求往往会发生变化,因此我们希望确保我们的组件尽可能稳定而灵活,以支持未来的变化,而不会因为过于复杂而牺牲其可维护性。

类比

当你看到上面的组件时,它立即就需要分成两个部分——内容和分页。
经过思考,我决定在这里使用卡片组的类比,这个比喻非常贴切,并且有助于我稍后正确确定每个部分的职责。
如果说内容是卡片组,那么分页就是翻阅卡片并选择显示哪张卡片的手。接下来,让我们牢记这一点:

图片描述

确定哪个“现实生活”类比最能描述我们的组件,对整个过程至关重要。你越能贴近眼前的挑战,你的解决方案就越好。在大多数情况下,处理“现实生活”的例子比抽象的编程设计理念更容易推理。
有了类比,我们就可以继续了。

分页组件

图片描述

让我们从最底层开始。什么是 Pagination 组件?
一个好方法是将其视为我们正在开发的整体组件范围之外的一个组件。Pagination 组件的作用是什么?

Pagination 组件的功能很简单——生成一个游标,仅此而已。
如果我们抛开所有生成这个游标的不同方式,我们就会意识到,这个组件的功能最终都归结于此。

事实上,生成游标的逻辑可以封装成一个 React hook,其 API 如下:

  • 设置Cursor(newCursor:number):void;
  • goNext():无效;
  • goPrev():无效;

在这个钩子接收的 props 中,它有一个onChange(currentCursor:number)回调函数,每当光标发生变化时都会调用该回调函数。 (你可以在这里
看到一个这样的钩子的例子

Pagination 组件只需使用此钩子并围绕它渲染一个具有所需交互性的 UI。根据我们的要求,Pagination 组件目前应支持以下属性:

  • 应该显示箭头:布尔值
  • 应该显示子弹:布尔值

(额外挑战:您将如何在这里实现更多的分页 UI?)

CardsDeck 组件

图片描述

和任何卡片组一样,你可能知道这个组件代表一叠卡片。
此时,定义 CardsDeck 的职责至关重要。CardsDeck
本质上就是一叠卡片。它知道或关心每张卡片代表什么吗?不。它应该从外部接收一个卡片数据列表(作为 prop),并为每个卡片创建一张卡片。

然而,它关注的是卡片如何在它们之间切换(过渡),所以我们明白这个组件的一个 prop 应该是我们感兴趣的过渡类型。我们的 CardsDeck 还应该接收一个 prop,指示现在应该显示哪张卡片,也就是一个光标。它并不关心是什么产生了这个光标,它尽可能地“愚蠢”。“给我一个光标,我就会显示一张卡片”。

以下是我们目前拥有的道具:

  • cardsData:卡片[];
  • 光标
  • 过渡类型:过渡类型;

(额外挑战:CardsDeck 是否应该验证给定的光标是否超出卡片列表长度的范围?)

包含动态内容的卡片。怎么做?

如前所述,CardsDeck 不应该感知每张卡片的内容,但为了操作卡片并在它们之间切换,它仍然需要对卡片进行某种控制。这意味着 CardsDeck 需要用一个 Card 包装器组件来包装每张卡片的内容:

图片描述

但是,既然每张卡片的实际渲染显然是在 CardsDeck 组件内部完成的,那么如何才能实现动态渲染内容呢?
一个选择是使用渲染属性,或者说“将子元素作为函数”——我们不再将 React 元素作为 CardsDeck 的子元素,而是使用一个函数。该函数将获取单张卡片(任意)的数据作为参数,并使用该数据返回一个 JSX 代码。
这样,我们就可以非常灵活地调整内容渲染方式,同时保留 CardsDeck 的功能。

解耦

Pagination 和 CardsDeck 组件都是独立组件。它们可以驻留在任何其他组件中,并且彼此完全解耦。这赋予了我们强大的功能,使我们能够在更多组件中复用代码,从而让我们的工作更加轻松,价值更高。
这种分离也使我们能够在各自的范围内进行修改,并且只要 API 保持完整,我们就可以确保使用它的组件的功能不会受到影响(暂且不谈视觉效果的回归)。

作品

图片描述

有了这两个组件后,就该将它们组合在一起了。
我们将 CardsDeck 和 Pagination 放在一个父组件中。CardsDeck 和 Pagination 组件共享光标,就这样!
这种组合方式让我们可以灵活地调整 CardsDeck 和 Pagination 的排列方式,并为父组件提供更多布局可能性。父组件也是决定是否显示分页的地方。

自动分页

到目前为止,除了最后一个需求,也就是自动分页之外,我们目前为止所实现的功能基本满足了所有需求。
真正的问题来了——哪个组件负责管理自动分页?
我们知道 CardsDeck 关注的是过渡类型(滑动、淡入淡出等)。那么它是否也应该关注自动分页呢?

让我们回到最初的类比——牌组和手。
如果我问你哪个负责一张一张地显示牌,答案显而易见。是手负责显示,而不是牌组。
所以,如果我们回到组件,很明显,Pagination 组件负责显示。更准确地说,是负责操作光标背后逻辑的部分——Pagination 钩子。

我们在分页钩子中添加了另一个属性,autoPaginate如果值为 true,它将自动开始移动光标。当然,如果我们有这样的属性,我们还需要在该钩子中至少公开一个方法来切换自动分页的开关:

  • toggleAutoPagination():void

现在我们需要将 CardsDeck 的悬停事件与自动分页的切换绑定起来。一个方案是让我们的 Pagination 组件暴露一个 prop,用于控制是否开启或关闭自动分页,并将其连接到父组件的某个状态。这样应该可以解决问题。

综上所述

在本文中,你了解了如何将组件转化为更贴近实际的案例,并将其提取为模块化部分,并清晰地定义关注点。
如果你能够更好地定义组件的边界,你的组件将更易于维护和复用,从而让你和你的产品/用户体验团队的工作更加轻松愉快。

与往常一样,如果您有其他您认为相关的技术或任何疑问,请务必与我们分享。

嘿!如果你喜欢刚才读到的内容,可以在推特上关注@mattibarzeev 🍻

Raphael SchallerUnsplash上拍摄的照片

文章来源:https://dev.to/mbarzeev/wisely-designing-your-react-components-4o0
PREV
在 VSCode 上设置 React 和 Vite:分步教程
NEXT
使用 TDD 创建 React 自定义钩子