Define a component with subcomponents and useImperativeHandle

92 Views Asked by At

I try to create a Component with SubComponents. The Component should use useImperativeHandle, so I need forwardRef.

My actual code looks like this:

const MyComponent = Object.assign(forwardRef(function(props, ref){
    useImperativeHandle(ref, () => ({
        toggle: function(){ //TODO}
    });
    return <div className="MyComponent" {...props}>
        {props.children || <MyComponent.Sub />}
    </div>
}),{
    Sub: function(props){
        return <div className="MyComponent_Sub">{props.children}</div>
    }
});
export default MyComponent;

I tried to add Typescript declarations.

For a normal Function Component I use the following declaration:

interface MyComponentProps extends PropsWithChildren {
    ...
}

const MyComponent: FC<MyComponentProps & HTMLProps<HTMLDivElement>> = function(props){
    return <div {...props}></div>
}

export default MyComponent;

For a Function Component with Sub Components I have found the following way: Subs always need to be declared with dot notation. Object.assign is not working.

interface MyComponentProps extends PropsWithChildren {
    ...
}

interface MyComponentSubs {
    Sub: FC<HTMLProps<HTMLSpanElement>>,
    ...
}

const MyComponent: FC<MyComponentProps & HTMLProps<HTMLDivElement>> & MyComponentSubs = function(props){
    return <div {...props}>
      {props.children || <MyComponent.Sub />}
    </div>
}

MyComponent.Sub = function(props){
    return <span {...props}>{props.children}</span>
}
export default MyComponent;

For a ForwardRef Component I tried the following, but props is not correctly resolved:

interface MyComponentProps extends PropsWithChildren {
    ...
}

interface MyComponentAPI {
    toggle: () => void
}

const MyComponent: ForwardRefExoticComponent<MyComponentProps & React.RefAttributes<MyComponentAPI>> = forwardRef(function(props, ref){
    useImperativeHandle(ref, () => ({
        toggle: function(){/*TODO*/}
    }));
    props.ref; //props.ref should not be available
    return <div {...props}>{props.children}</div>
})

But I only get it working like this:

interface MyComponentProps extends PropsWithChildren {
    ...
}

interface MyComponentAPI {
    toggle: () => void
}

const MyComponent = forwardRef<MyComponentAPI, MyComponentProps>(function(props, ref){
    useImperativeHandle(ref, () => ({
        toggle: function(){/*TODO*/}
    }));
    return <div {...props}>{props.children}</div>
})

But I have no idea, how to define a ForwardRef Component with SubComponents, I tried the following but did not get this running:

const MyForwardComponentWithSubs: ForwardRefExoticComponent<MyComponentProps> & MyComponentSubs = forwardRef(function(props, ref){
    useImperativeHandle(ref, () => ({
        toggle: function(){/*TODO*/}
    }));

    return <div {...props}>{props.children}</div>
});

MyForwardComponentWithSubs.Sub = function(props){
    return createElement("span", props, props.children);
}

Is it possible to use Object.assign like in the first code block? Or is there an even simpler way?

There is a TS Playgroud available here

1

There are 1 best solutions below

0
Victor Gorban On

I suppose you shouldn't use Object.assign here (especially in TS code), as you cannot provide sufficient type declarations for it. What are you trying to do? Do you want to have an object of components here? Just create a type for it, or use something built-in like Record <string, React.ForwardRefExoticComponent>

Here is some working code (If you need an object of components, then just create another type for it):

// usage: useRef<ImperativeProps> ();
export type ImperativeProps = {
  externalSetStayActive: (isActive: boolean) => void;
};

export type ComponentProps = {
  scalingMultiplier?: number;
  activeZIndex?: number;
  inactiveZIndex?: number;
  className?: string;
  style?: Record<string, any>;
  htmlElement?: string;
  children: React.ReactNode;
  /** extra properties */
  [key: string]: any;
};

const FormBlock = React.forwardRef<
  ImperativeProps,
  ComponentProps
>(function Component(
  {
    scalingMultiplier = 1,
    activeZIndex = 2,
    inactiveZIndex = 1,
    className,
    htmlElement = "label",
    style,
    children,
    ...otherProps
  }: ComponentProps,
  elRef
) {
// you component logic. 
}) as React.ForwardRefExoticComponent<
ComponentProps & React.RefAttributes<ImperativeProps>
>;

export default FormBlock;

If that is not enough and you need more examples, you can try and use my project here (TS, JS, Storybook). Beware of russian comments.