Skip to content
This page has been auto-translated and may contain errors.View in English

子组件和组合

HTML 从来就是通过嵌套来工作的:<select> 包裹它的 <option><ul> 包裹它的 <li><button> 包裹它的标签。中间的元素是外层元素的子元素,两者配合工作才能呈现你看到的效果。React 组件也可以用同样的方式工作。与其通过具名 props 传递所有东西,一个组件可以像原生元素那样包裹内容,而这个单一的想法就是区分一个你**配置的组件和一个你组合**的组件的关键。

本章像课程的其他部分一样,通过为组件库构建 Button 来展开论述:先是 children,然后是 prop 展开,最后是让组件在实际应用中可复用的收尾工作。

Children 作为接口

Props 章节介绍过 children:组件的开始标签和结束标签之间的内容会作为一个名叫 children 的 prop 传入。它为复用性带来的收益比看起来要大得多。

jsx
function Button({ children }) {
  return <button>{children}</button>
}

// 使用:<Button>立即购买</Button>

组件决定了 children 在哪里渲染,调用方决定了它们是什么。比较一下用 text prop 的情况,它在调用方只想要文本的时候还可以,但如果调用方想要超过文本的东西,它就不行了。有了 children,调用方就可以传递任何东西:

jsx
// 使用:
// <Button>
//   <CartIcon />
//   立即购买
// </Button>

在标签左边加一个图标,不需要 icon prop,不需要 iconPosition="left" 选项,组件作者甚至不需要预见这些。想把图标放在右边?把它移到文本的另一侧就行了。组件作者原本要发明的每一个配置 prop 都消解了,调用方只需要写出他们想要的标记。

转发其他 props

渲染原生 <button>Button 还有第二个问题:事件处理。当调用方写 <Button onClick={...}> 时,这个 onClick 是一个自定义 prop 落在你的组件上;除非组件把它传给底层的真实 <button>,否则什么都不会发生。你可以手动转发 onClick,然后转发 onDoubleClick,再转发 style,再转发 className,但仍然覆盖不了原生 button 接受的所有东西。展开语法可以一次性转发它们全部:

jsx
function Button({ children, ...rest }) {
  return <button {...rest}>{children}</button>
}

解构你的组件实际处理的 props,用 rest 语法收集其他的,然后把 rest 展开到底层元素上。调用方传递的每一个有效 button 属性,包括事件处理器,都落在了它该去的地方。你的组件为自己发明的 props(比如 variant)被解构出去,永远不会到达 DOM 元素。

为你自己的 API 留出空间

可复用的组件通常在原生元素的基础上增加选项。本课程的 button 使用 size"sm""lg")和 variant"success""warning""danger"),每个都映射到一个 CSS 类

jsx
function Button({ children, size, variant, className, ...rest }) {
  const sizeClass = size ? `button-${size}` : ''
  const variantClass = variant ? `button-${variant}` : ''
  const allClasses = [sizeClass, variantClass, className].filter(Boolean).join(' ')

  return (
    <button className={allClasses} {...rest}>
      {children}
    </button>
  )
}

这个合并避免了一个微妙的 bug。如果组件设置了 className 而调用方也传递了一个,无论哪个最后落到元素上就赢了:在你自己的 className 之后展开 rest,调用方的类会无声地覆盖你的;在之前展开,你的会覆盖他们的。把 className 解构出来并自己合并字符串是解决办法。clsxclassnames 这样的库就是专门做这个合并的,当条件增多时尤其有用。

展开 rest props 是一个约定,它有一些尖锐的边缘。这个部分已经展示了其中两个,展开顺序和泄漏的 props,是比较轻的问题,虽然泄漏比大多数人预期的要安静。

泄漏的 prop 实际上做什么

variant 这样的小写字符串 prop 会无警告地落在 DOM 节点上成为一个无效的 HTML 属性,这是会出问题的情况。React 会对像 iconPosition 这样的驼峰命名 prop 发出警告并仍然渲染它;对布尔值或像 onclick 这样的全小写处理器名称会发出警告并什么都不渲染。像 onClik 这样的驼峰命名打字错误既没有属性也没有警告。

ref 则反过来:在 React 19 中它是一个普通 prop,所以它会跟随 ...rest 对函数组件起作用,无需 forwardRef 就能到达底层元素。

昂贵的边界是 API 面积。一旦调用方可以传递任意原生 props,他们就会这样做,他们调用的每一个都成了你在下一个版本欠他们的东西;即使你从未文档化过,之后收窄传递也是一个破坏性改变。

课程的 Avatar "大挑战"展示了组合的另一面:一个组件以三种不同方式渲染,当提供了 src 时显示照片,当有 children 时显示首字母缩写,否则显示图标。这样的重载适合文档化的组件库,其中一个灵活的 Avatar 胜过三个几乎相同的组件。在应用代码中,过早倾向于它通常表示一个组件在做太多工作;两个小组件胜过一个聪明的。

这两个本能之间的拉力,添加 prop 还是从外部组合,贯穿了这一部分的其余部分,而下一些模式,复合组件渲染 props,是对它的结构化答案。

Juno让调用方把东西放进去 一个渲染 children 的 Button 不需要 text prop、icon prop 和 icon-position prop。无论谁使用它,都在标签里面按他们想要的顺序写上图标和文本,Button 就会把这些内容放在该去的地方。

嵌套,这是 HTML 一直在做的事,也是 React 最好的复用技巧。

Juno让调用方把东西放进去 在两个习惯的基础上构建可复用组件:在内容应该去的地方渲染 children,解构你处理的 props,同时把 ...rest 展开到底层元素上,这样原生属性和事件处理器就能传递过去。

自己合并 className 而不是让展开顺序决定哪个存活下来,并把像 variant 这样发明的 props 排除在 rest 对象之外,这样它们永远不会到达 DOM。

Juno让调用方把东西放进去 把 rest-prop 转发视为你的组件 API 面积的一部分:冲突通过展开顺序解决,未解构的自定义 props 泄漏到 DOM,调用方会依赖这个传递。

在 prop 获得它的位置之前更喜欢通过 children 组合而不是配置 props,把重载组件留给文档化的库代码,在那里一个灵活的东西确实能替换多个。

接下来:复合组件,一个组件变成多个协同工作的组件。