Búsqueda de sitios web

reactjs - ¿Cómo tipar el forwardRef de React con useImperativeHandle en TypeScript?


Aquí hay una versión simplificada de un componente Tabs que expone un método llamado activateTab a través de useImperativeHandle:

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);

// Usage:

const tabs = [{ name: "Tab 1" }, { name: "Tab 2" }] as const;

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

Todo está bien hasta este punto. Aquí hay un CodeSandbox que funciona.

Pero, ahora quiero agregar un componente Tabs.Panel para que el uso sea:

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

Intenté lo siguiente, pero TypeScript se queja:

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; }>>'

¿Cuál es la mejor manera de lograr esta API Tabs.Panel en TypeScript?

CodeSandbox que no funciona

Respuesta #1

Necesitas usar Object.assign:

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

type PanelProps = {
  children: ReactNode;
};

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

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 = Object.assign(forwardRef(TabsComponent), { Panel });

const tabs = [{ name: "Tab 1" }, { name: "Tab 2" }] as const;

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

Parque infantil

Aquí puedes encontrar más explicación sobre el uso de propiedades estáticas en funciones

Artículos relacionados