Skip to content

如何在范型组件中使用 forwardRef

English

在TypeScript中使用React的forwardRef时,存在一些限制,其中最大的限制是它禁用了对泛型组件的类型推断。

什么是泛型组件?

泛型组件的一个常见用途是Table组件:

tsx
const Table = <T,>(props: {
  data: T[];
  renderRow: (row: T) => React.ReactNode;
}) => {
  return (
    <table>
      <tbody>
        {props.data.map((item, index) => (
          <props.renderRow key={index} {...item} />
        ))}
      </tbody>
    </table>
  );
};
const Table = <T,>(props: {
  data: T[];
  renderRow: (row: T) => React.ReactNode;
}) => {
  return (
    <table>
      <tbody>
        {props.data.map((item, index) => (
          <props.renderRow key={index} {...item} />
        ))}
      </tbody>
    </table>
  );
};

这里,当我们传递一个数组到data时,它会推断出传递给renderRow函数的参数的类型。

tsx
<Table
  {/* 1. Data is a string here... */}
  data={["a", "b"]}
  {/* 2. So ends up inferring as a string in renderRow. */}
  renderRow={(row) => {
    return <tr>{row}</tr>;
  }}
/>;
 
<Table
  {/* 3. Data is a number here... */}
  data={[1, 2]}
  {/* 4. So ends up inferring as a number in renderRow. */}
  renderRow={(row) => {
    return <tr>{row}</tr>;
  }}
/>;
<Table
  {/* 1. Data is a string here... */}
  data={["a", "b"]}
  {/* 2. So ends up inferring as a string in renderRow. */}
  renderRow={(row) => {
    return <tr>{row}</tr>;
  }}
/>;
 
<Table
  {/* 3. Data is a number here... */}
  data={[1, 2]}
  {/* 4. So ends up inferring as a number in renderRow. */}
  renderRow={(row) => {
    return <tr>{row}</tr>;
  }}
/>;

这对于无需任何额外注解即可在renderRow函数上获得类型推断非常有帮助。

使用forwardRef的问题

问题出现在当我们尝试给我们的Table组件添加一个ref时:

tsx
const Table = <T,>(
  props: {
    data: T[];
    renderRow: (row: T) => React.ReactNode;
  },
  ref: React.ForwardedRef<HTMLTableElement>
) => {
  return (
    <table ref={ref}>
      <tbody>
        {props.data.map((item, index) => (
          <props.renderRow key={index} {...item} />
        ))}
      </tbody>
    </table>
  );
};

const ForwardReffedTable = React.forwardRef(Table);
const Table = <T,>(
  props: {
    data: T[];
    renderRow: (row: T) => React.ReactNode;
  },
  ref: React.ForwardedRef<HTMLTableElement>
) => {
  return (
    <table ref={ref}>
      <tbody>
        {props.data.map((item, index) => (
          <props.renderRow key={index} {...item} />
        ))}
      </tbody>
    </table>
  );
};

const ForwardReffedTable = React.forwardRef(Table);

到目前为止,这一切看起来都很好,但是当我们使用我们的ForwardReffedTable组件时,之前看到的类型推断不再工作。

tsx
<ForwardReffedTable
  {/* 1. Data is a string here... */}
  data={["a", "b"]}
  {/* 2. But ends up being inferred as unknown. */}
  renderRow={(row) => {
    return <tr />;
  }}
/>;
 
<ForwardReffedTable
  {/* 3. Data is a number here... */}
  data={[1, 2]}
  {/* 4. But still ends up being inferred as unknown. */}
  renderRow={(row) => {
    return <tr />;
  }}
/>;
<ForwardReffedTable
  {/* 1. Data is a string here... */}
  data={["a", "b"]}
  {/* 2. But ends up being inferred as unknown. */}
  renderRow={(row) => {
    return <tr />;
  }}
/>;
 
<ForwardReffedTable
  {/* 3. Data is a number here... */}
  data={[1, 2]}
  {/* 4. But still ends up being inferred as unknown. */}
  renderRow={(row) => {
    return <tr />;
  }}
/>;

这非常令人沮丧。但是,这个问题可以被解决。

解决方案

我们可以使用不同的类型定义来重新定义forwardRef,然后它就开始工作了。

这是新的定义:

ts
function fixedForwardRef<T, P = {}>(
  render: (props: P, ref: React.Ref<T>) => React.ReactNode
): (props: P & React.RefAttributes<T>) => React.ReactNode {
  return React.forwardRef(render) as any;
}
function fixedForwardRef<T, P = {}>(
  render: (props: P, ref: React.Ref<T>) => React.ReactNode
): (props: P & React.RefAttributes<T>) => React.ReactNode {
  return React.forwardRef(render) as any;
}

我们可以改变我们的定义来使用fixedForwardRef

ts
const ForwardReffedTable = fixedForwardRef(Table);
const ForwardReffedTable = fixedForwardRef(Table);

突然之间,它就开始工作了:

tsx
<Table
  {/* 1. Data is a string here... */}
  data={["a", "b"]}
  {/* 2. So ends up inferring as a string in renderRow. */}
  renderRow={(row) => {
    return <tr>{row}</tr>;
  }}
/>;
 
<Table
  {/* 3. Data is a number here... */}
  data={[1, 2]}
  {/* 4. So ends up inferring as a number in renderRow. */}
  renderRow={(row) => {
    return <tr>{row}</tr>;
  }}
/>;
<Table
  {/* 1. Data is a string here... */}
  data={["a", "b"]}
  {/* 2. So ends up inferring as a string in renderRow. */}
  renderRow={(row) => {
    return <tr>{row}</tr>;
  }}
/>;
 
<Table
  {/* 3. Data is a number here... */}
  data={[1, 2]}
  {/* 4. So ends up inferring as a number in renderRow. */}
  renderRow={(row) => {
    return <tr>{row}</tr>;
  }}
/>;

这是我的推荐解决方案 - 用实际工作的不同类型的新函数重新定义forwardRef。

Vue 3中的泛型组件示例

在Vue 3中,类型推断和引用的传递与React有所不同。Vue 3提供了Composition API,这是一种新的组件逻辑组织方式,它与TypeScript的集成让我们可以更灵活地处理类型推断和引用。以下是如何在Vue 3中处理类似问题的示例。

假设我们有一个类似的泛型Table组件,但在Vue 3的上下文中:

vue
<template>
  <table>
    <tbody>
      <tr v-for="(item, index) in data" :key="index">
        <slot :row="item"></slot>
      </tr>
    </tbody>
  </table>
</template>

<script lang="ts">
import { defineComponent, PropType } from 'vue';

export default defineComponent({
  props: {
    data: {
      type: Array as PropType<any[]>,
      required: true,
    },
  },
});
</script>
<template>
  <table>
    <tbody>
      <tr v-for="(item, index) in data" :key="index">
        <slot :row="item"></slot>
      </tr>
    </tbody>
  </table>
</template>

<script lang="ts">
import { defineComponent, PropType } from 'vue';

export default defineComponent({
  props: {
    data: {
      type: Array as PropType<any[]>,
      required: true,
    },
  },
});
</script>

在这个组件中,我们使用了slot来代替React示例中的renderRow函数,这样父组件就可以决定如何渲染每一行。这种方法保留了类型推断的灵活性,并且适用于Vue的模板系统。

在Vue 3中,如果你想要传递一个引用(例如,到一个DOM元素或另一个组件实例),你可能会使用ref和响应式APIprovide/inject

vue
<script setup lang="ts">
import { ref, provide } from 'vue';

const tableRef = ref(null);
provide('tableRef', tableRef);
</script>
<script setup lang="ts">
import { ref, provide } from 'vue';

const tableRef = ref(null);
provide('tableRef', tableRef);
</script>

然后在需要的地方注入引用:

vue
<script setup lang="ts">
import { inject } from 'vue';

const tableRef = inject('tableRef');
</script>
<script setup lang="ts">
import { inject } from 'vue';

const tableRef = inject('tableRef');
</script>

Updated Date:

Light tomorrow with today.