不要再像这样构建你的 UI 组件了❌

2025-05-24

不要再像这样构建你的 UI 组件了❌

确实,每个人都为将代码库中最常被复制粘贴的代码抽象成可复用的组件而感到兴奋。但这样做的一个坏处就是草率的抽象,这留到以后再说吧,今天的主题是学习如何制作真正可复用的组件。

很多时候,在抽象可复用组件时,它会变成一堆乱七八糟的 props。你可能见过一些“可复用”组件,其 props 超过 50 个!这些组件最终会变得极其难以使用和维护,同时还会带来性能问题和难以追踪的实际 bug。

为新用例添加一个 prop 不仅仅是一个 if 语句,最终你会在组件中做出很多更改,从而导致代码大小变得巨大且难以维护。

但是如果我们留意我们所创建的抽象类型,那么我们就可以创造出真正易于使用和维护的东西,没有错误,并且不会太大而让用户支付下载惩罚。

Kent C dodd 已经深入解释了这个问题,可以看看:
Simply React

可重用组件是什么样的?

我们有一个LoginFormModal组件,它抽象了登录和注册表单的模态框。组件本身并不复杂,只接受少量的 props,但它相当不灵活,我们需要在整个应用程序中创建更多的模态框,所以我们需要一个更灵活的组件。

<LoginFormModal
  onSubmit={handleSubmit}
  modalTitle="Modal title"
  modalLabelText="Modal label (for screen readers)"
  submitButton={<button>Submit form</button>}
  openButton={<button>Open Modal</button>}
/>

Enter fullscreen mode Exit fullscreen mode

最后,我们将创建可以像这样使用的组件:

<Modal>
  <ModalOpenButton>
    <button>Open Modal</button>
  </ModalOpenButton>
  <ModalContents aria-label="Modal label (for screen readers)">
    <ModalDismissButton>
      <button>Close Modal</button>
    </ModalDismissButton>
    <h3>Modal title</h3>
    <div>Some great contents of the modal</div>
  </ModalContents>
</Modal>
Enter fullscreen mode Exit fullscreen mode

但这难道不比仅仅传递 prop 更复杂吗?
我们把责任转移给了组件的使用者,而不是创建者,这被称为控制反转。它肯定比我们现有的代码要多LoginFormModal,但它更简单、更灵活,并且能够适应我们未来的用例,而不会变得更加复杂。

例如,假设我们不想只渲染表单,而是
想渲染任何我们想要渲染的内容。我们的Modal组件支持这一点,但
LoginFormModal需要接受一个新的 prop。或者,如果我们想让关闭
按钮显示在内容下方怎么办?我们需要一个名为 的特殊 prop
renderCloseBelow。但有了我们的组件Modal,一切都变得显而易见。你只需将
ModalCloseButton组件移动到你想要的位置即可。

更加灵活,API 表面积更少。

这种模式称为复合组件——组件协同工作以形成完整的UI。HTML中的<select>和就是一个典型的例子。<option>

它广泛应用于许多现实世界的图书馆,例如:

让我们在构建可重用的同时创建我们的第一个复合组件modal

构建我们的第一个复合组件

import * as React from 'react'
import VisuallyHidden from '@reach/visually-hidden'

/* Here the Dialog and CircleButton is a custom component 
Dialog is nothing button some styles applied on reach-dialog 
component provided by @reach-ui */
import {Dialog, CircleButton} from './lib'

const ModalContext = React.createContext()
//this helps in identifying the context while visualizing the component tree
ModalContext.displayName = 'ModalContext'

function Modal(props) {
  const [isOpen, setIsOpen] = React.useState(false)

  return <ModalContext.Provider value={[isOpen, setIsOpen]} {...props} />
}

function ModalDismissButton({children: child}) {
  const [, setIsOpen] = React.useContext(ModalContext)
  return React.cloneElement(child, {
    onClick: () => setIsOpen(false),
  })
}

function ModalOpenButton({children: child}) {
  const [, setIsOpen] = React.useContext(ModalContext)
  return React.cloneElement(child, {
    onClick: () => setIsOpen(true),
})
}

function ModalContentsBase(props) {
  const [isOpen, setIsOpen] = React.useContext(ModalContext)
  return (
    <Dialog isOpen={isOpen} onDismiss={() => setIsOpen(false)} {...props} />
  )
}

function ModalContents({title, children, ...props}) {
  return (
    //we are making generic reusable component thus we allowed user custom styles
   //or any prop they want to override
    <ModalContentsBase {...props}>
      <div>
        <ModalDismissButton>
          <CircleButton>
            <VisuallyHidden>Close</VisuallyHidden>
            <span aria-hidden>×</span>
          </CircleButton>
        </ModalDismissButton>
      </div>
      <h3>{title}</h3>
      {children}
    </ModalContentsBase>
  )
}

export {Modal, ModalDismissButton, ModalOpenButton, ModalContents}
Enter fullscreen mode Exit fullscreen mode

太棒了!我们完成了不少工作,现在我们可以像这样使用上面的组件了:

<Modal>
     <ModalOpenButton>
         <Button>Login</Button>
     </ModalOpenButton>
     <ModalContents aria-label="Login form" title="Login">
         <LoginForm
            onSubmit={register}
            submitButton={<Button>Login</Button>}
          />
      </ModalContents>
  </Modal>
Enter fullscreen mode Exit fullscreen mode

代码现在更具可读性和灵活性。

优雅、华丽的代码

额外奖励:允许用户传递自己的 onClickHandler

设置其子按钮的ModalOpenButton 以便我们可以打开和关闭模态框。但是,如果 这些组件的用户希望在点击按钮时(除了 打开/关闭模态框之外)执行某些操作(例如,触发分析),该怎么办?ModalCloseButtononClick


我们希望创建一个 callAll 方法,运行传递给它的所有方法,如下所示:

callAll(() => setIsOpen(false), ()=>console.log("I ran"))
Enter fullscreen mode Exit fullscreen mode

这是我在 Kent 的Epic React 研讨会上学到的。这个方法太巧妙了,我太喜欢了。

const callAll = (...fns) => (...args) => fns.forEach(fn => fn && fn(...args))
Enter fullscreen mode Exit fullscreen mode

让我们在我们的组件中使用它:

function ModalDismissButton({children: child}) {
  const [, setIsOpen] = React.useContext(ModalContext)
  return React.cloneElement(child, {
    onClick: callAll(() => setIsOpen(false), child.props.onClick),
  })
}

function ModalOpenButton({children: child}) {
  const [, setIsOpen] = React.useContext(ModalContext)
  return React.cloneElement(child, {
    onClick: callAll(() => setIsOpen(true), child.props.onClick),
  })
}
Enter fullscreen mode Exit fullscreen mode

可以通过将一个传递onClickHandler给我们的自定义按钮来使用该电源,如下所示:

<ModalOpenButton>
  <button onClick={() => console.log('sending data to facebook ;)')}>Open Modal</button>
</ModalOpenButton>
Enter fullscreen mode Exit fullscreen mode

庆祝的家伙

结论

不要草率地进行抽象,也不要把所有东西都留给 props。也许它现在只是一个简单的组件,但你不知道将来需要覆盖哪些用例,不要将其视为时间和可维护性之间的权衡,复杂性可能会呈指数级增长。

使用复合组件增强 React 中的组合能力,让您的生活更轻松。

另外,请查看 Kent 的Epic React 课程,我在其中学习了复合组件模式等更多内容。

关于我,我叫 Harsh,热爱编程。我从 16 岁就开始编程了。使用 React 构建 Web 应用让我感觉很自在。我目前正在学习Remix

如果你喜欢这个博客,欢迎联系我们!我计划以后带来更多类似的博客。

推特
领英

进一步了解我:Harsh choudhary

查看我的测试钩子博客或如何构建通用自定义钩子博客。

文章来源:https://dev.to/harshkc/stop-building-your-ui-components-like-this-19il
PREV
程序员不该做的事情——两年团队工作经验总结
NEXT
Mistakes I made while learning Web Development as a beginner Not taking breaks Not Building Projects Not using Developer Tools Not taking help from internet and developer communities