Definitelytyped: React: using `forwardRef` with multiple HOCs

Created on 30 May 2019  Â·  20Comments  Â·  Source: DefinitelyTyped/DefinitelyTyped

forwardRef is designed to allow HOCs to pass through a ref to the composed component: https://reactjs.org/docs/forwarding-refs.html#forwarding-refs-in-higher-order-components

With the latest version of the React types, this can be done like so:

import * as React from 'react';

const myHoc1 = <Props extends unknown>(
    Component: React.ComponentClass<Props>,
) => {
    type ComponentInstance = InstanceType<typeof Component>;
    const MyHoc = React.forwardRef<ComponentInstance, Props>((props, ref) => (
        <Component ref={ref} {...props} />
    ));
    return MyHoc;
};

class MyComponent extends React.Component<{}> {}

const MyComponent2 = myHoc1(MyComponent);

What if we want to wrap the component with multiple HOCs (as in commonly the case)? Each of these HOCs may choose to forward refs.

If we try to do that, we get an unexpected type error:

import * as React from 'react';

const myHoc1 = <Props extends unknown>(
    Component: React.ComponentClass<Props>,
) => {
    type ComponentInstance = InstanceType<typeof Component>;
    const MyHoc = React.forwardRef<ComponentInstance, Props>((props, ref) => (
        <Component ref={ref} {...props} />
    ));
    return MyHoc;
};

const myHoc2 = <Props extends unknown>(
    Component: React.ComponentClass<Props>,
) => {
    type ComponentInstance = InstanceType<typeof Component>;
    const MyHoc = React.forwardRef<ComponentInstance, Props>((props, ref) => (
        <Component ref={ref} {...props} />
    ));
    return MyHoc;
};

class MyComponent extends React.Component<{}> {}

const MyComponent2 = myHoc1(MyComponent);
/*
Unexpected type error:
Argument of type 'ForwardRefExoticComponent<RefAttributes<Component<{}, any, any>>>' is not assignable to parameter of type 'ComponentClass<{ ref: ((instance: Component<{}, any, any> | null) => void) | RefObject<Component<{}, any, any>> | null; key: ReactText; }, any>'.
  Type 'ForwardRefExoticComponent<RefAttributes<Component<{}, any, any>>>' provides no match for the signature 'new (props: { ref: ((instance: Component<{}, any, any> | null) => void) | RefObject<Component<{}, any, any>> | null; key: ReactText; }, context?: any): Component<{ ref: ((instance: Component<{}, any, any> | null) => void) | RefObject<...> | null; key: ReactText; }, any, any>'.
*/
const MyComponent3 = myHoc2(MyComponent2);

If you know how to fix the issue, make a pull request instead.

  • [x] I tried using the @types/xxxx package and had problems.
  • [x] I tried using the latest stable version of tsc. https://www.npmjs.com/package/typescript
  • [x] I have a question that is inappropriate for StackOverflow. (Please ask any appropriate questions there).
  • [x] [Mention](https://github.com/blog/821-mention-somebody-they-re-notified) the authors (see Definitions by: in index.d.ts) so they can respond.

    • Authors: @johnnyreilly @bbenezech @pzavolinsky @digiguru @ericanderson @tkrotoff @DovydasNavickas @onigoetz @theruther4d @guilhermehubner @ferdaber @jrakotoharisoa @pascaloliv @hotell @franklixuefei @Jessidhia @saranshkataria @lukyth @eps1lon

If you do not mention the authors the issue will be ignored.

Most helpful comment

Same with the overload for ForwardRefExoticComponent. Fix:

-function hoc<T, P extends { ref?: React.Ref<T> }>(
+function hoc<P extends { ref?: React.Ref<any> }>(
     Component: React.ForwardRefExoticComponent<P>,
 ): React.ForwardRefExoticComponent<P>;

All together now, with the fixes from above:

function hoc<T extends React.ComponentClass<any>>(
    Component: T,
): React.ForwardRefExoticComponent<
    React.ComponentPropsWithoutRef<T> & { ref?: React.Ref<InstanceType<T>> }
>;
function hoc<P extends { ref?: React.Ref<any> }>(
    Component: React.ForwardRefExoticComponent<P>,
): React.ForwardRefExoticComponent<P>;
function hoc<P>(
    Component: React.FunctionComponent<P>,
): React.ForwardRefExoticComponent<P>;
function hoc<P>(Component: React.ComponentType<P>) {
    // I don't know how to implement this without breaking out of the types.
    // The overloads are ensuring correct usage, so we should be good?
    // @ts-ignore
    return React.forwardRef((props, ref) => <Component ref={ref} {...props} />);
}

declare const FuncComponent: React.FC<{ foo?: string }>;
declare const FuncComponentWithRefProp: React.FC<{
    foo?: string;
    ref: React.Ref<any>;
}>;
declare const KlassComponent: React.ComponentClass<{ foo?: string }, {}>;
const PreviouslyForwardedComponent = React.forwardRef(
    (props: { foo?: string }, ref: React.Ref<HTMLDivElement>) => (
        <div ref={ref} />
    ),
);

const Test1 = hoc(FuncComponent);
<Test1 ref={{} as any} />; // this one fails as expected
const Test1b = hoc(FuncComponentWithRefProp);
<Test1b ref={{} as any} />;
const Test2 = hoc(KlassComponent);
<Test2 ref={{} as any} />;
const Test3 = hoc(PreviouslyForwardedComponent);
<Test3 ref={{} as any} />;
const DoubleUpTest1 = hoc(Test2);
<DoubleUpTest1 ref={{} as any} />;
const DoubleUpTest2 = hoc(Test3);
<DoubleUpTest2 ref={{} as any} />;

All 20 comments

I guess my question is: how to define a HOC such that it can received a ~RefForwardingComponent~ForwardRefExoticComponent in addition to a ComponentClass?

Perhaps:

const myHoc = <T extends React.ComponentClass<unknown>, Props extends unknown>(
    Component:
        | React.ComponentClass<Props>
        | React.ForwardRefExoticComponent<Props>,
) => {
    type ComponentInstance = InstanceType<T>;
    const MyHoc = React.forwardRef<ComponentInstance, Props>((props, ref) => (
        <Component ref={ref} {...props} />
    ));
    return MyHoc;
};

Although that's pretty ugly, and I'm sure it's wrong in many ways too… 😨

HOCs should use React.JSXElementConstructor which is the most general form of what React will accept to render JSX elements. Exotic components, class components, and function components should all be subtypes of JSXElementConstructor

IIUC, forwardRef should only be used with ComponentClass and ~RefForwardingComponent~ForwardRefExoticComponent. The other component type—function components—do not support refs. Therefore, our HOC should only operate on those two component types.

Ideally our HOC would support all component types, but only forward refs if the composed component is one of the supporting types, but that's for another day…

As far as "React best practices go," an HOC should not care what type of component it's wrapping, however like you said it's difficult to tell whether it can forward its ref or not. Best way is probably to use an overload signature, one that catches class components and one that catches ref forwarding components. IMO I don't really think it's that dirty since a lot of people have been using ComponentType with HOCs and that's already a union of function and class component types.

I agree, however before I begin to try tackling function components, I'd first like to make it work with a subset of JSXElementConstructor: that is, ComponentClass and ~RefForwardingComponent~ForwardRefExoticComponent.

Is this correct? 🤔 https://github.com/DefinitelyTyped/DefinitelyTyped/issues/35834#issuecomment-497329600

Once I've nailed that, I can think about handling function components as well.

It's slightly more complex than that. For all but class components, the ref props is part of its props interface and uses "regular" TypeScript logic (non-JSX typechecking), but for class components we rely on JSX.IntrinsicClassAttributes, so we need a special signature for it. Try having this signature (not sure what the implementation signature should look like, I usually just any everything for the implementation signature):

declare function hoc<P, T extends React.ComponentClass<P>>(
  Component: T
): React.ForwardRefExoticComponent<P & { ref?: React.Ref<InstanceType<T>> }>
declare function hoc<P extends { ref?: React.Ref<any> }>(
  Component: React.JSXElementConstructor<P>
): React.ForwardRefExoticComponent<P>

I did test this out with various usage patterns, and only the function component fails:

declare function hoc<P, T extends React.ComponentClass<P>>(
  Component: T
): React.ForwardRefExoticComponent<P & { ref?: React.Ref<InstanceType<T>> }>
declare function hoc<P extends { ref?: React.Ref<any> }>(
  Component: React.JSXElementConstructor<P>
): React.ForwardRefExoticComponent<P>

declare const FuncComponent: React.FC<{ foo?: string }>
declare const KlassComponent: React.ComponentClass<{ foo?: string }, {}>
const PreviouslyForwardedComponent = React.forwardRef(
  (props: { foo?: string }, ref: React.Ref<HTMLDivElement>) => <div ref={ ref } />
)

const Test1 = hoc(FuncComponent) // this one fails as expected
const Test2 = hoc(KlassComponent)
const Test3 = hoc(PreviouslyForwardedComponent)
const DoubleUpTest1 = hoc(Test2)
const DoubleUpTest2 = hoc(Test3)

Nice, thanks!

I tweaked it slightly to prevent function components from being passed if they happen to have a ref prop. Now, the HOC only supports ComponentClass + ForwardRefExoticComponent:

import * as React from 'react';

declare function hoc<P, T extends React.ComponentClass<P>>(
    Component: T,
): React.ForwardRefExoticComponent<P & { ref?: React.Ref<InstanceType<T>> }>;
declare function hoc<T, P extends { ref?: React.Ref<T> }>(
    Component: React.ForwardRefExoticComponent<P>,
): React.ForwardRefExoticComponent<P>;

declare const FuncComponent: React.FC<{ foo?: string; }>;
declare const FuncComponentWithRefProp: React.FC<{ foo?: string; ref: React.Ref<any> }>;
declare const KlassComponent: React.ComponentClass<{ foo?: string }, {}>;
const PreviouslyForwardedComponent = React.forwardRef(
    (props: { foo?: string }, ref: React.Ref<HTMLDivElement>) => (
        <div ref={ref} />
    ),
);

const Test1 = hoc(FuncComponent); // this one fails as expected
const Test1b = hoc(FuncComponentWithRefProp); // this one fails as expected
const Test2 = hoc(KlassComponent);
const Test3 = hoc(PreviouslyForwardedComponent);
const DoubleUpTest1 = hoc(Test2);
const DoubleUpTest2 = hoc(Test3);

I'm going to have a go at implementing it now…

Added an overload for FunctionComponent. The actual implementation is simple—it's a shame about the @ts-ignore though. I have 0 idea how to avoid that.

function hoc<P, T extends React.ComponentClass<P>>(
    Component: T,
): React.ForwardRefExoticComponent<P & { ref?: React.Ref<InstanceType<T>> }>;
function hoc<T, P extends { ref?: React.Ref<T> }>(
    Component: React.ForwardRefExoticComponent<P>,
): React.ForwardRefExoticComponent<P>;
function hoc<P>(
    Component: React.FunctionComponent<P>,
): React.ForwardRefExoticComponent<P>;
function hoc<P>(Component: React.ComponentType<P>) {
    // I don't know how to implement this without breaking out of the types.
    // The overloads are ensuring correct usage, so we should be good?
    // @ts-ignore
    return React.forwardRef((props, ref) => <Component ref={ref} {...props} />);
}

It would be nice if the implementation only used forwardRef if the composed component was an applicable type (ComponentClass + ForwardRefExoticComponent), but I think that's impossible since there's no way to know at runtime what the component type is (AFAIK). Most React folks don't seem bothered by this—they just use forwardRef in their HOCs, regardless of types.

So (assuming I've got it right) this is what HOCs which use forwardRef should look like in TS (in order to cover all possible usages). That's quite a lot of boilerplate, especially if you have a lot of HOCs…

forwardRef won't throw anyway when used on a function component, it will just make the ref null, so I think it's consistent as far as implementation goes.

And yes unfortunately this is the best we got until we get some form of higher-kinded types so we can dynamically produce overload signatures :(

One thing this doesn't cover is host elements though you probably won't be using HOCs that often against host elements. If you do want that you'd need another overload signature for stringly-typed components

I'm curious what that overload would look like. I've needed to do that a few times with HOCs and ended up wrapping the host component in a custom component, just to satisfy the types…

function hoc<K extends keyof JSX.IntrinsicElements>(
  Component: K
): React.ForwardRefExoticComponent<JSX.IntrinsicElements[K]>

function hoc<P, T extends React.ComponentClass<P>>(
    Component: T,
): React.ForwardRefExoticComponent<P & { ref?: React.Ref<InstanceType<T>> }>;
function hoc<T, P extends { ref?: React.Ref<T> }>(
    Component: React.ForwardRefExoticComponent<P>,
): React.ForwardRefExoticComponent<P>;
function hoc<P>(
    Component: React.FunctionComponent<P>,
): React.ForwardRefExoticComponent<P>;

Thanks!

Note I noticed a bug with the class overload: the P (props) generic is always inferred as {} in 3.4 (and unknown in 3.5).

image

Reduced test case:

declare function hoc<P, T extends React.ComponentClass<P>>(Component: T): any;

declare const KlassComponent: React.ComponentClass<{ foo: string }, {}>;

// 3.4.5: Property 'foo' is missing in type '{}' but required in type '{ foo: string; }'.
// 3.5.1: Property 'foo' is missing in type 'Readonly<unknown>' but required in type 'Readonly<{ foo: string; }>'.
hoc(KlassComponent);

I thought TS would infer the P generic from its usage in T, but apparently not:

type Component<P> = { p: P };
declare function hoc<P, T extends Component<P>>(t: T): Component<P>;

declare const Component: Component<{ foo: string }>;
// Expected type: Component<{ foo: string }>
// Actual type: Component<unknown>
const Component2 = hoc(Component);

image

We can rewrite the class overload to avoid the P generic by using ComponentPropsWithoutRef on T instead:

declare function hoc<T extends React.ComponentClass<any>>(
    Component: T,
): React.ForwardRefExoticComponent<React.ComponentPropsWithoutRef<T> & { ref?: React.Ref<InstanceType<T>> }>;

Same with the overload for ForwardRefExoticComponent. Fix:

-function hoc<T, P extends { ref?: React.Ref<T> }>(
+function hoc<P extends { ref?: React.Ref<any> }>(
     Component: React.ForwardRefExoticComponent<P>,
 ): React.ForwardRefExoticComponent<P>;

All together now, with the fixes from above:

function hoc<T extends React.ComponentClass<any>>(
    Component: T,
): React.ForwardRefExoticComponent<
    React.ComponentPropsWithoutRef<T> & { ref?: React.Ref<InstanceType<T>> }
>;
function hoc<P extends { ref?: React.Ref<any> }>(
    Component: React.ForwardRefExoticComponent<P>,
): React.ForwardRefExoticComponent<P>;
function hoc<P>(
    Component: React.FunctionComponent<P>,
): React.ForwardRefExoticComponent<P>;
function hoc<P>(Component: React.ComponentType<P>) {
    // I don't know how to implement this without breaking out of the types.
    // The overloads are ensuring correct usage, so we should be good?
    // @ts-ignore
    return React.forwardRef((props, ref) => <Component ref={ref} {...props} />);
}

declare const FuncComponent: React.FC<{ foo?: string }>;
declare const FuncComponentWithRefProp: React.FC<{
    foo?: string;
    ref: React.Ref<any>;
}>;
declare const KlassComponent: React.ComponentClass<{ foo?: string }, {}>;
const PreviouslyForwardedComponent = React.forwardRef(
    (props: { foo?: string }, ref: React.Ref<HTMLDivElement>) => (
        <div ref={ref} />
    ),
);

const Test1 = hoc(FuncComponent);
<Test1 ref={{} as any} />; // this one fails as expected
const Test1b = hoc(FuncComponentWithRefProp);
<Test1b ref={{} as any} />;
const Test2 = hoc(KlassComponent);
<Test2 ref={{} as any} />;
const Test3 = hoc(PreviouslyForwardedComponent);
<Test3 ref={{} as any} />;
const DoubleUpTest1 = hoc(Test2);
<DoubleUpTest1 ref={{} as any} />;
const DoubleUpTest2 = hoc(Test3);
<DoubleUpTest2 ref={{} as any} />;

any idea about this ?

Was this page helpful?
0 / 5 - 0 ratings