在本文中,我们将讨论如何优雅地包装第三方 React 组件,并介绍一个具体实例:如何包装 react-photo-album 中导出的 <PhotoAlbum /> 组件。
我们有两个需求
- 给
<PhotoAlbum />组件的某些属性设置预设值,但不影响其他属性。 - 为
<PhotoAlbum />组件添加React.memo()层。
需求一:设置预设值
首先,我们想给 <PhotoAlbum /> 组件的 layout、columns 和 spacing 属性增加预设值,但又不希望影响其他的 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() 层,并使用类型断言来处理泛型问题,保证了组件在使用时不会出现泛型相关的错误。