skip to content

React
如何包装第三方 React 组件

在本文中,我们将讨论如何优雅地包装第三方 React 组件,并介绍一个具体实例:如何包装 react-photo-album 中导出的 <PhotoAlbum /> 组件。

我们有两个需求

  1. <PhotoAlbum /> 组件的某些属性设置预设值,但不影响其他属性。
  2. <PhotoAlbum /> 组件添加 React.memo() 层。

需求一:设置预设值

首先,我们想给 <PhotoAlbum /> 组件的 layoutcolumnsspacing 属性增加预设值,但又不希望影响其他的 props 属性。

最初的尝试如下:

import { memo } from 'react'
import PhotoAlbum from 'react-photo-album'

import type { Photo, PhotoAlbumProps } from 'react-photo-album'

const IDEAL_COLUMN_WIDTH = 150

function CustomPhotoAlbum<T extends Photo>(props: PhotoAlbumProps<T>) {
  const {
    layout = 'masonry',
    columns = (containerWidth: number) => Math.floor(containerWidth / IDEAL_COLUMN_WIDTH),
    spacing = 16,
    ...restProps
  } = props
  return <PhotoAlbum layout={layout} columns={columns} spacing={spacing} {...restProps} />
}

然而,这样的代码在使用时会报错,提示 layout 属性缺失。这是因为 layout 属性在 PhotoAlbumProps<T> 中是 required 的。尽管我们为其增加了预设值,但由于使用了原有的类型声明,所以在使用时仍然要求 layout 属性。

为了解决这个问题,我们需要将 <CustomPhotoAlbum /> 组件的 layout 属性变为可选的,同时不影响其他属性的类型。

这里我们可以使用类型编程的方法来实现这个目标。

import { memo } from 'react'
import PhotoAlbum from 'react-photo-album'

import type { Photo, PhotoAlbumProps } from 'react-photo-album'

const IDEAL_COLUMN_WIDTH = 150

+ type PartialSubset<T, K extends keyof T> = Partial<Pick<T, K>> & Omit<T, K>

- function CustomPhotoAlbum<T extends Photo>(props: PhotoAlbumProps<T>) {
+ function CustomPhotoAlbum<T extends Photo>(props: PartialSubset<PhotoAlbumProps<T>, 'layout'>) {
  const {
    layout = 'masonry',
    columns = (containerWidth: number) => Math.floor(containerWidth / IDEAL_COLUMN_WIDTH),
    spacing = 16,
    ...restProps
  } = props
  return <PhotoAlbum layout={layout} columns={columns} spacing={spacing} {...restProps} />
}

在上述代码中,我们定义了一个泛型函数类型 PartialSubset<T, K extends keyof T>,它接受一个类型 T 和一个属性键集合 K 作为参数,并返回一个新的类型。这个新类型是将类型 T 中指定属性 K 变为可选的,并保留其他属性的类型构成的。通过使用这个类型,我们将 <CustomPhotoAlbum /> 组件的 layout 属性变为可选的,而其他属性不受任何影响。

现在使用 <CustomPhotoAlbum photos={photos} onClick={onClick} /> 时,不会再提示 layout 属性缺失的问题。

需求二:添加 React.memo()

第二个需求是给 <PhotoAlbum /> 组件添加 React.memo() 层,以提高性能。

在解决这个需求时,我们面临了一个比较棘手的问题。React.memo() 会影响组件的泛型,如果直接使用 React.memo() 进行包裹,可能会导致组件在使用时出现泛型相关的问题。

通过参考 issuecomment-65659662,我们找到了一个合适的方式来处理 React.memo() 对泛型的影响,即使用类型断言来处理,具体做法如下:

const MemoizedComponent = React.memo(InnerComponent) as typeof InnerComponent

综上所述,最终的代码如下:

import { memo } from 'react'
import PhotoAlbum from 'react-photo-album'

import type { Photo, PhotoAlbumProps } from 'react-photo-album'

const IDEAL_COLUMN_WIDTH = 150

type PartialSubset<T, K extends keyof T> = Partial<Pick<T, K>> & Omit<T, K>

function CustomPhotoAlbum<T extends Photo>(props: PartialSubset<PhotoAlbumProps<T>, 'layout'>) {
  const {
    layout = 'masonry',
    columns = (containerWidth: number) => Math.floor(containerWidth / IDEAL_COLUMN_WIDTH),
    spacing = 16,
    ...restProps
  } = props
  return <PhotoAlbum layout={layout} columns={columns} spacing={spacing} {...restProps} />
}

+ export default memo(CustomPhotoAlbum) as typeof CustomPhotoAlbum

在上述代码中,我们成功地给 <CustomPhotoAlbum /> 组件添加了 React.memo() 层,并使用类型断言来处理泛型问题,保证了组件在使用时不会出现泛型相关的错误。