Type errors when extending component more than one level using forwardRef and useImperativeHandle
Asked Answered
G

1

7

I'm experimenting with extending components in React. I'm trying to extend Handsontable using forwardRef and useImperativeHandle. First I wrap Handsontable in my own BaseTable component, adding some methods. Then I extend the BaseTable in a CustomersTable component in the same way to add even more methods and behavior. Everything seems to work well until I try to consume the CustomersTable in CustomersTableConsumer where I get some type errors. The component works just fine, it's just Typescript that isn't happy.

BaseTable:

export type BaseTableProps = {
  findReplace: (v: string, rv: string) => void;
} & HotTable;

export const BaseTable = forwardRef<BaseTableProps, HotTableProps>(
  (props, ref) => {
    const hotRef = useRef<HotTable>(null);

    const findReplace = (value: string, replaceValue: string) => {
      const hot = hotRef?.current?.__hotInstance;
      // ...
    };

    useImperativeHandle(
      ref,
      () =>
        ({
          ...hotRef?.current,
          findReplace
        } as BaseTableProps)
    );

    const gridSettings: Handsontable.GridSettings = {
      autoColumnSize: true,
      colHeaders: true,
      ...props.settings
    };

    return (
      <div>
        <HotTable
          {...props}
          ref={hotRef}
          settings={gridSettings}
        />
      </div>
    );
  }
);

CustomersTable:

export type CustomerTableProps = HotTable & {
  customerTableFunc: () => void;
};

export const CustomersTable = forwardRef<CustomerTableProps, BaseTableProps>(
  (props, ref) => {
    const baseTableRef = useRef<BaseTableProps>(null);

    const customerTableFunc = () => {
      console.log("customerTableFunc");
    };

    useImperativeHandle(
      ref,
      () =>
        ({
          ...baseTableRef?.current,
          customerTableFunc
        } as CustomerTableProps)
    );

    useEffect(() => {
      const y: Handsontable.ColumnSettings[] = [
        {
          title: "firstName",
          type: "text",
          wordWrap: false
        },
        {
          title: "lastName",
          type: "text",
          wordWrap: false
        }
      ];

      baseTableRef?.current?.__hotInstance?.updateSettings({
        columns: y
      });
    }, []);

    return <BaseTable {...props} ref={baseTableRef} />;
  }
);

CustomerTableConsumer:

export const CustomerTableConsumer = () => {
  const [gridData, setGridData] = useState<string[][]>([]);
  const customersTableRef = useRef<CustomerTableProps>(null);

  const init = async () => {
    const z = [];
    z.push(["James", "Richard"]);
    z.push(["Michael", "Irwin"]);
    z.push(["Solomon", "Beck"]);

    setGridData(z);

    customersTableRef?.current?.__hotInstance?.updateData(z);
    customersTableRef?.current?.customerTableFunc();
    customersTableRef?.current?.findReplace("x", "y");  };

  useEffect(() => {
    init();
  }, []);

  // can't access extended props from handsontable on CustomersTable
  return <CustomersTable data={gridData} ref={customersTableRef} />;
};

Here is a Codesandbox example.

How do I need to update my typings to satisfy Typescript in this scenario?

Gherkin answered 18/12, 2022 at 9:39 Comment(2)
import HotTable, { HotTableProps } from "@handsontable/react"; And export type CustomerTableProps = { customerTableFunc: () => void; } & HotTable; fixed the first issueAccursed
codesandbox.io/s/pedantic-joliot-ev7jpc?file=/src/…Accursed
C
4

You need to specify the type of the ref for forwardRef. This type is used then later in useRef<>(). It's confusing, because HotTable is used in useRef<HotTable>(), but BaseTable can't be used the same way, as it is a functional component and because forwardRef was used in BaseTable. So, basically, for forwardRef we define a new type and then later use that in useRef<>(). Note the distinction between BaseTableRef and BaseTableProps.

Simplified example

export type MyTableRef = {
  findReplace: (v: string, rv: string) => void;
};

export type MyTableProps = { width: number; height: number };

export const MyTable = forwardRef<MyTableRef, MyTableProps>(...);

// then use it in useRef
const myTableRef = useRef<MyTableRef>(null);
<MyTable width={10} height={20} ref={myTableRef} />

Final solution

https://codesandbox.io/s/hopeful-shape-h5lvw7?file=/src/BaseTable.tsx

BaseTable:

import HotTable, { HotTableProps } from "@handsontable/react";
import { registerAllModules } from "handsontable/registry";
import { forwardRef, useImperativeHandle, useRef } from "react";
import Handsontable from "handsontable";

export type BaseTableRef = {
  findReplace: (v: string, rv: string) => void;
} & HotTable;

export type BaseTableProps = HotTableProps;

export const BaseTable = forwardRef<BaseTableRef, BaseTableProps>(
  (props, ref) => {
    registerAllModules();
    const hotRef = useRef<HotTable>(null);

    const findReplace = (value: string, replaceValue: string) => {
      const hot = hotRef?.current?.__hotInstance;
      // ...
    };

    useImperativeHandle(
      ref,
      () =>
        ({
          ...hotRef?.current,
          findReplace
        } as BaseTableRef)
    );

    const gridSettings: Handsontable.GridSettings = {
      autoColumnSize: true,
      colHeaders: true,
      ...props.settings
    };
    return (
      <div>
        <HotTable
          {...props}
          ref={hotRef}
          settings={gridSettings}
          licenseKey="non-commercial-and-evaluation"
        />
      </div>
    );
  }
);

CustomersTable:

import Handsontable from "handsontable";
import React, {
  forwardRef,
  useEffect,
  useImperativeHandle,
  useRef
} from "react";
import { BaseTable, BaseTableRef, BaseTableProps } from "./BaseTable";

export type CustomerTableRef = {
  customerTableFunc: () => void;
} & BaseTableRef;

export type CustomerTableProps = BaseTableProps;

export const CustomersTable = forwardRef<CustomerTableRef, CustomerTableProps>(
  (props, ref) => {
    const baseTableRef = useRef<BaseTableRef>(null);

    const customerTableFunc = () => {
      console.log("customerTableFunc");
    };

    useImperativeHandle(
      ref,
      () =>
        ({
          ...baseTableRef?.current,
          customerTableFunc
        } as CustomerTableRef)
    );

    useEffect(() => {
      const y: Handsontable.ColumnSettings[] = [
        {
          title: "firstName",
          type: "text",
          wordWrap: false
        },
        {
          title: "lastName",
          type: "text",
          wordWrap: false
        }
      ];

      baseTableRef?.current?.__hotInstance?.updateSettings({
        columns: y
      });
    }, []);

    return <BaseTable {...props} ref={baseTableRef} />;
  }
);

CustomerTableConsumer:

import { useEffect, useRef, useState } from "react";
import { CustomersTable, CustomerTableRef } from "./CustomerTable";

export const CustomerTableConsumer = () => {
  const [gridData, setGridData] = useState<string[][]>([]);
  const customersTableRef = useRef<CustomerTableRef>(null);

  // Check console and seee that customerTableFunc from customersTable,
  // findReplace from BaseTable and __hotInstance from Handsontable is available
  console.log(customersTableRef?.current);

  const init = async () => {
    const z = [];
    z.push(["James", "Richard"]);
    z.push(["Michael", "Irwin"]);
    z.push(["Solomon", "Beck"]);

    setGridData(z);

    customersTableRef?.current?.__hotInstance?.updateData(z);
    customersTableRef?.current?.customerTableFunc();
  };

  useEffect(() => {
    init();
  }, []);

  return <CustomersTable data={gridData} ref={customersTableRef} />;
};

In your sandbox example it's almost correct, just fix the props type for CustomersTable. I would recommend though to not use Props suffix for ref types, as it is very confusing.

https://codesandbox.io/s/unruffled-framework-1xmltj?file=/src/CustomerTable.tsx

export const CustomersTable = forwardRef<CustomerTableProps, HotTableProps>(...)
Calida answered 21/12, 2022 at 19:49 Comment(1)
Fantastic, thank you so much! Making the distinction of props and refs makes things so much clearer.Gherkin

© 2022 - 2024 — McMap. All rights reserved.