You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在TypeScript中为带useImperativeHandle的forwardRef Tabs组件添加Panel子组件?

在TypeScript中实现Tabs.Panel子组件的最优方式

问题背景

已实现一个通过useImperativeHandle暴露activateTab方法的Tabs组件,代码如下:

import React, { forwardRef, useImperativeHandle, Ref, ReactNode } from 'react';

type TabsProps<TabName> = {
  tabs: readonly { name: TabName }[];
  children: ReactNode;
};

const TabsComponent = <TabName extends string>(
  props: TabsProps<TabName>,
  ref: Ref<{ activateTab: (tabName: TabName) => void }>
) => {
  const { tabs, children } = props;

  useImperativeHandle(ref, () => ({
    activateTab: (tabName: TabName) => {}
  }));

  return (
    <div>
      <div role="tablist">
        {tabs.map(({ name }) => (
          <button type="button" role="tab" key={name}>
            {name}
          </button>
        ))}
      </div>
      {children}
    </div>
  );
};

const Tabs = forwardRef(TabsComponent);

// 原有用法
const tabs = [{ name: "Tab 1" }, { name: "Tab 2" }] as const;

export default function App() {
  return (
    <Tabs tabs={tabs}>
      {...}
    </Tabs>
  );
}

现在希望添加Tabs.Panel子组件,使用法变为:

export default function App() {
  return (
    <Tabs tabs={tabs}>
      <Tabs.Panel>Content</Tabs.Panel>
    </Tabs>
  );
}

但直接挂载Panel到Tabs时,TypeScript报错:

type PanelProps = {
  children: ReactNode;
};

const Panel = ({ children }: PanelProps) => {
  return <div role="tabpanel">{children}</div>;
};

Tabs.Panel = Panel;
     ~~~~~
       ^
       Property 'Panel' does not exist on type 'ForwardRefExoticComponent<TabsProps<string> & RefAttributes<{ activateTab: (tabName: string) => void; }>>'

解决方案

要实现这种带命名子组件的API,核心是扩展ForwardRef组件的类型定义,同时保留原有的泛型和ref类型支持,以下是最优实现步骤:

1. 定义完整的Tabs组件类型

创建包含主组件和子组件的类型,确保泛型和ref类型能正确传递:

// 定义Panel组件的Props
type PanelProps = {
  children: ReactNode;
};

// 定义Tabs组件的完整类型:主组件类型 + 静态子组件Panel
type TabsComponent<TabName extends string> = React.ForwardRefExoticComponent<
  TabsProps<TabName> & React.RefAttributes<{ activateTab: (tabName: TabName) => void }>
> & {
  Panel: React.FC<PanelProps>;
};

2. 实现Panel组件

编写Panel组件逻辑,可根据需求添加更多属性(比如关联tabName):

const Panel = ({ children }: PanelProps) => {
  return <div role="tabpanel">{children}</div>;
};

3. 类型断言并挂载子组件

由于forwardRef返回的类型不包含静态属性,需要用类型断言将Tabs指定为我们定义的完整类型,再挂载Panel:

// 断言类型并挂载Panel
const Tabs = forwardRef(TabsComponent) as unknown as TabsComponent<string>;
Tabs.Panel = Panel;

4. 泛型传递优化(可选)

如果需要让Tabs的泛型TabName传递给Panel(比如Panel需要绑定对应tab),可调整类型定义:

// 调整PanelProps,添加tabName关联
type PanelProps<TabName extends string> = {
  tabName: TabName;
  children: ReactNode;
};

// 调整TabsComponent类型,让Panel复用相同的TabName泛型
type TabsComponent<TabName extends string> = React.ForwardRefExoticComponent<
  TabsProps<TabName> & React.RefAttributes<{ activateTab: (tabName: TabName) => void }>
> & {
  Panel: React.FC<PanelProps<TabName>>;
};

// 此时Panel用法可绑定tabName
<Tabs tabs={tabs}>
  <Tabs.Panel tabName="Tab 1">Tab 1 Content</Tabs.Panel>
  <Tabs.Panel tabName="Tab 2">Tab 2 Content</Tabs.Panel>
</Tabs>

完整示例代码

import React, { forwardRef, useImperativeHandle, Ref, ReactNode } from 'react';

type TabsProps<TabName> = {
  tabs: readonly { name: TabName }[];
  children: ReactNode;
};

type PanelProps = {
  children: ReactNode;
};

// 定义Tabs组件的完整类型
type TabsComponent<TabName extends string> = React.ForwardRefExoticComponent<
  TabsProps<TabName> & React.RefAttributes<{ activateTab: (tabName: TabName) => void }>
> & {
  Panel: React.FC<PanelProps>;
};

const Panel = ({ children }: PanelProps) => {
  return <div role="tabpanel">{children}</div>;
};

const TabsComponent = <TabName extends string>(
  props: TabsProps<TabName>,
  ref: Ref<{ activateTab: (tabName: TabName) => void }>
) => {
  const { tabs, children } = props;

  useImperativeHandle(ref, () => ({
    activateTab: (tabName: TabName) => {
      console.log(`切换到标签:${tabName}`);
    }
  }));

  return (
    <div>
      <div role="tablist">
        {tabs.map(({ name }) => (
          <button type="button" role="tab" key={name}>
            {name}
          </button>
        ))}
      </div>
      {children}
    </div>
  );
};

// 断言类型并挂载Panel
const Tabs = forwardRef(TabsComponent) as unknown as TabsComponent<string>;
Tabs.Panel = Panel;

// 使用示例
const tabs = [{ name: "Tab 1" }, { name: "Tab 2" }] as const;

export default function App() {
  return (
    <Tabs tabs={tabs}>
      <Tabs.Panel>Tab 1 内容</Tabs.Panel>
      <Tabs.Panel>Tab 2 内容</Tabs.Panel>
    </Tabs>
  );
}

关键点说明

  • 核心是通过类型断言扩展forwardRef返回的组件类型,添加静态子组件定义,保证类型安全。
  • 若需要泛型传递,需确保主组件和子组件的泛型参数一致,避免类型不兼容。
  • 这种设计符合React生态常见的组件API规范,兼具易用性和类型严谨性。

内容的提问来源于stack exchange,提问作者Misha Moroshko

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.18 09:31:13