reactjstypescripthandsontable

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


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?


Solution

  • 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>(...)