Aaron Balthaser
Aaron Balthaser

Reputation: 2644

How to attach a compound component when using React forward ref (property does not exist on forwardRefExoticComponent)

I have a Panel component that optionally allows the user to use compound components. The panel has a default header that contains the close functionality (button) and the main reason I did this is was to give the user the option to use a custom header with their own button and whatever else they would like, so I opted to use compound components and to allow the Panel component to check for the existence of the component in its children and if so, it will not use the default header. I have it all working but I can't seem to figure out how to get the Typescript error to clear.

Below is the code pertaining to Typescript and the error I'm getting. I also linked to a sandbox with the working code with errors.

I have tried tons of variations from Google searches and in a days time cannot find the solution.

Sandbox: https://codesandbox.io/s/panel-513yq?file=/src/Panel/Panel.tsx:510-4061

App.js:

export default function App() {
  const [open, setOpen] = React.useState(false);

  const handleClose = () => {
    setOpen(false);
  };

  const handleOpen = () => {
    setOpen(true);
  };

  return (
    <div id="page" data-test-id="component-app">
      <header id="header">
        <button onClick={handleOpen}>Open</button>
      </header>
      <main id="main">
        <Panel open={open} onClosed={handleClose} renderPortal={true}>
          <Panel.Header>
            <div>
              <button onClick={handleClose}>Close</button>
            </div>
          </Panel.Header>
          <Panel.Content>Panel Content</Panel.Content>
        </Panel>
      </main>
    </div>
  );
}

Panel Component:

interface ModalPropsComposition {
  Header?: React.FC<PanelHeaderProps>;
  Content?: React.FC<PanelContentProps>;
}

export interface PanelProps
  extends React.HTMLAttributes<HTMLDivElement>,
    ModalPropsComposition {
  open: boolean;
  position?: string;
  renderPortal?: boolean;

  // Lifecycle callbacks
  onClose?: () => void;
  onClosed?: () => void;
  onOpen?: () => void;
  onOpened?: () => void;
}

/**
 * @Codeannex UI React: Panel Component
 *
 * Panel Component
 */
const _Panel = ({
  children,
  open,
  renderPortal,
  onClose,
  onClosed,
  onOpen,
  onOpened
}: PanelProps & { forwardedRef: React.Ref<HTMLDivElement> }): JSX.Element => {
  // panel code...
};

export const Panel = React.forwardRef(
  (props: PanelProps, ref: React.Ref<HTMLDivElement>): JSX.Element => {
    return <_Panel {...props} forwardedRef={ref} data-testid={PANEL_TEST_ID} />;
  }
);

Panel.Header = PanelHeader;
Panel.Content = PanelContent;

Error:

enter image description here

Upvotes: 6

Views: 5500

Answers (2)

whygee
whygee

Reputation: 1984

By doing in Panel.tsx

Panel.Content = PanelContent;
Panel.Header = PanelHeader 

typescript complains since the properties Content and Header are custom and don't exist on the base forwardRef type.

To fix it declare a custom interface that extends forwardRef base type and add your custom properties Header and Content:

interface IPanel
  extends React.ForwardRefExoticComponent<
    PanelProps & React.RefAttributes<HTMLDivElement>
  > {
  Header: typeof PanelHeader;
  Content: typeof PanelContent;
}

and use it like this:

const forwardRef = React.forwardRef<HTMLDivElement, PanelProps>(
  (props, ref): JSX.Element => {
    return (
      <PanelComponent
        {...props}
        forwardedRef={ref}
        data-testid={PANEL_TEST_ID}
      />
    );
  }
);

export const Panel = {
  ...forwardRef,
  Header: PanelHeader,
  Content: PanelContent
} as IPanel;

Or... just do export const Panel: any but you lose type checking by doing this

Working CodeSandbox forked from yours.

Upvotes: 10

jtbandes
jtbandes

Reputation: 118741

You can fix this error by using Object.assign. This works because Object.assign is typed as assign<T, U>(target: T, source: U): T & U;, so the types T and U can be inferred, and the return type becomes a mix of the forwardRef exotic component type and the object with Header and Content properties.

export const Panel = Object.assign(
  React.forwardRef(
    (props: PanelProps, ref: React.Ref<HTMLDivElement>): JSX.Element => {
      return (
        <PanelComponent
          {...props}
          forwardedRef={ref}
          data-testid={PANEL_TEST_ID}
        />
      );
    }
  ),
  {
    Header: PanelHeader,
    Content: PanelContent
  }
);

Upvotes: 12

Related Questions