Vue3+TSX中使用defineComponent定义泛型组件的更佳方案
Vue3 泛型组件在defineComponent下的更优实现方案
问题背景
在Vue3函数式组件模式下,可轻松定义泛型组件,示例代码如下:
type Loading = { status: 'LOADING' } type Error = { status: 'ERROR'; message: string } type AsyncData<T> = Loading | { status: 'LOADED'; value: T } | Error type Props<T> = { data: AsyncData<T> renderItem: (item: T) => JSX.Element | null } const LoadingElement = <T,>(props: Props<T>) => { return ( <div class={`${props.data.status === 'LOADING' ? styles['top-level'] : ''}`} style={props.data.status !== 'LOADED' ? 'display: flex; justify-content: center' : ''}> {props.data.status === 'LOADED' ? props.renderItem(props.data.value) : null} {props.data.status === 'ERROR' ? ( <div> <HelpTooltip icon="error-warning" text={props.data.message} /> </div> ) : null} {props.data.status === 'LOADING' ? <div class={styles.spinner}></div> : null} </div> ) }
使用该组件时,item可获得正确的类型提示。
但当使用defineComponent(需用到ref、watch、computed等API)时,无法直接为组件定义泛型。此前采用的hack方案较为繁琐:
const GenericComponentHack = <T>() => defineComponent<Props<T>>({ setup(props) => { return () => <OtherComponent {...props} /> }) export const GenericComponent = <T>(props: Props<T>): JSX.Element => { const GenericComponentHackInstance = GenericComponentHack<T>() // @ts-ignore return <GenericComponentHackInstance {...props} /> }
更优实现方式
方案1:函数包裹defineComponent(无需@ts-ignore)
直接返回带泛型参数的组件函数,避免额外实例化和类型忽略:
export const GenericComponent = defineComponent(<T>() => ({ props: { data: { type: Object as () => AsyncData<T>, required: true }, renderItem: { type: Function as () => (item: T) => JSX.Element | null, required: true } }, setup(props) { // 可正常使用ref、watch、computed等响应式API const someRef = ref(null) watch(() => props.data, (newData) => { // 处理数据变化逻辑 }) return () => ( <div> {props.data.status === 'LOADED' ? props.renderItem(props.data.value) : null} {/* 其余渲染逻辑 */} </div> ) } }))
方案2:Vue 3.3+ 原生支持defineProps泛型
Vue 3.3及以上版本中,defineProps原生支持泛型参数,无需额外hack:
import { defineComponent, defineProps, ref, watch } from 'vue' export const GenericComponent = <T>() => defineComponent({ setup() { const props = defineProps<Props<T>>() // 正常使用响应式API const count = ref(0) watch(() => props.data, () => { count.value++ }) return () => ( <div> {props.data.status === 'LOADED' ? props.renderItem(props.data.value) : null} {/* 其余渲染逻辑 */} </div> ) } })
使用组件时可显式传递泛型:
<GenericComponent<MyDataType> data={myData} renderItem={(item) => <div>{item.name}</div>} />
方案3:带响应式逻辑的函数式组件
若组件内部状态无需持久化,可直接给函数式组件添加泛型,同时在内部使用响应式API:
export const GenericComponent = <T>(props: Props<T>) => { // 注意:此处ref、computed等会在每次渲染时创建新实例,仅适用于无持久化状态的场景 const isLoading = computed(() => props.data.status === 'LOADING') return () => ( <div class={`${isLoading.value ? styles['top-level'] : ''}`}> {props.data.status === 'LOADED' ? props.renderItem(props.data.value) : null} {/* 其余渲染逻辑 */} </div> ) }
注:若组件需要持久化内部状态,优先选择前两种结合defineComponent的方案。
内容的提问来源于stack exchange,提问作者michael_bitard
相关产品推荐
相关产品推荐

