Flow: React's Higher-order Components documentation needs an example of a component that consumes properties

Created on 31 Aug 2017  路  9Comments  路  Source: facebook/flow

I read the documentation in https://flow.org/en/docs/react/hoc/ but I have a hard time figuring out how to type higher order components that need specific props, as opposed to the ones that inject props like the documentation says.

Here is my (non working) code, modified from the documentation:

import * as React from 'react';


type NeededProps = {
  bar: number
};

function consumeProp<Props: {}>(
  Component: React.ComponentType<Props>
): React.ComponentType<NeededProps & Props> {
  return function WrapperComponent(props: NeededProps & Props) {
    if (props.bar < 100) {
        return <Component {...props} />;
    } 
    return null;
  };
}

class MyComponent extends React.Component<{
  a: number,
  b: number,
}> {}

const MyEnhancedComponent = consumeProp(MyComponent);

<MyEnhancedComponent a={1} b={2} bar={42} />;

But I still get errors:

    13:         return <Component {...props} />;
                    ^ props of React element `Component`. This type is incompatible with
    8: function consumeProp<Props: {}>(
       ^ some incompatible instantiation of `Props`
    13:         return <Component {...props} />;
                    ^ props of React element `Component`. This type is incompatible with the expected param type of
    8: function consumeProp<Props: {}>(
       ^ some incompatible instantiation of `Props`

See the live try.

I tried using utilities like $Diff, or { ...$Exact<Something>, ... SomethingElse } but I couldn't make it work.

Some guidance would be helpful, including maybe some more documentation :)

(Note: my ultimate goal is to type a HOC that both consumes and injects properties).

Most helpful comment

And now, here the example of a HOC that both consumes and injects properties (previous example adapted with the good documentation for injected components):

import * as React from 'react';


type NeededProps = {
  bar: number
};

type InjectedProps = {
  foo: string
};

function consumeProp<Props: NeededProps>(
  Component: React.ComponentType<InjectedProps & $Supertype<Props>>
): React.ComponentType<Props> {
  return function WrapperComponent(props: Props) {
    if (props.bar < 100) {
        return <Component {...props} foo="string" />;
    } 
    return null;
  };
}

class MyComponent extends React.Component<{
  a: number,
  b: number,
  foo: string
}> {}

const MyEnhancedComponent = consumeProp(MyComponent);

<MyEnhancedComponent a={1} b={2} bar={42} />;

All 9 comments

Here is another try, possibly easier to follow because I used the same terminology for "input" and "output" than in the doc, but gives the same error:

import * as React from 'react';


type NeededProps = {
  bar: number
};

function consumeProp<InputProps: {}, OutputProps: NeededProps & InputProps>(
  Component: React.ComponentType<InputProps>
): React.ComponentType<OutputProps> {
  return function WrapperComponent(props: OutputProps) {
    if (props.bar < 100) {
        return <Component {...props} />;
    } 
    return null;
  };
}

class MyComponent extends React.Component<{
  a: number,
  b: number,
}> {}

const MyEnhancedComponent = consumeProp(MyComponent);

<MyEnhancedComponent a={1} b={2} bar={42} />;

Here is the error:

    13:         return <Component {...props} />;
                    ^ props of React element `Component`. This type is incompatible with
    8: function consumeProp<InputProps: {}, OutputProps: NeededProps & InputProps>(
       ^ some incompatible instantiation of `InputProps`
    13:         return <Component {...props} />;
                    ^ props of React element `Component`. This type is incompatible with the expected param type of
    8: function consumeProp<InputProps: {}, OutputProps: NeededProps & InputProps>(
       ^ some incompatible instantiation of `InputProps`

This is really confusing to me. If OutputProps is both InputProps and NeededProps (intersection type), why is there an error when returning <Component {...props} /> when Component needs just InputProps ? Even taking variance into account, shouldn't this be always right, whatever InputProps is ?

Using try's introspection mechanism (in bottom status bar), I see that props is {} & { bar: number }, which is the opposite that OutputProps is (I mean: { bar: number } && {}). I don't know if this is important. The documentation for HOC seems to imply that the order in an intersection is important, but the documentation for intersections says nothing about this.

Component is ComponentType<InputProps> which is expected.

So I think the problem here is that OutputProps has nothing about InputProps: flow replaced InputProps with {} in its defition. I believe this is a bug...

In case anybody is interested, I managed to make flow pass with this:

import * as React from 'react';


type NeededProps = {
  bar: number
};

function consumeProp<Props: NeededProps>(
  Component: React.ComponentType<$Supertype<Props>>
): React.ComponentType<Props> {
  return function WrapperComponent(props: Props) {
    if (props.bar < 100) {
        return <Component {...props} />;
    } 
    return null;
  };
}

class MyComponent extends React.Component<{
  a: number,
  b: number,
}> {}

const MyEnhancedComponent = consumeProp(MyComponent);

<MyEnhancedComponent a={1} b={2} bar={42} />;

Notice the use of $Supertype. It means that the props for MyEnhancedComponent are extending the props for MyComponent -- which in Flow means that they have more properties, so it makes sense.

I think this could be easier, especially I still think my first approach should work.

And now, here the example of a HOC that both consumes and injects properties (previous example adapted with the good documentation for injected components):

import * as React from 'react';


type NeededProps = {
  bar: number
};

type InjectedProps = {
  foo: string
};

function consumeProp<Props: NeededProps>(
  Component: React.ComponentType<InjectedProps & $Supertype<Props>>
): React.ComponentType<Props> {
  return function WrapperComponent(props: Props) {
    if (props.bar < 100) {
        return <Component {...props} foo="string" />;
    } 
    return null;
  };
}

class MyComponent extends React.Component<{
  a: number,
  b: number,
  foo: string
}> {}

const MyEnhancedComponent = consumeProp(MyComponent);

<MyEnhancedComponent a={1} b={2} bar={42} />;

@julienw I recently ran into this issue as well, and I might have found a solution that works for your code from https://github.com/facebook/flow/issues/4759#issuecomment-327417887:

See example

function consumeProp<InputProps: {}, OutputProps: InputProps & NeededProps>(
  Component: React.ComponentType<InputProps>
): React.ComponentType<OutputProps> {
  return function WrapperComponent(props: OutputProps) {
    const { bar, ...otherProps } = props;
    if (bar < 100) {
        return <Component {...otherProps} />;
    } 
    return null;
  };
}
  1. You had an error in your example: you were sending all OutputProps to the Component you were wrapping, but that was declared to only accept InputProps. So you need to desctructure the bar from props and send the rest of the props to Component.

  2. I switched the order of the OutputProps intersection to InputProps & NeededProps and that seems to make Flow happy. If the order is reversed, Flow throws an error still - even with the fix from 1.

Thanks for your comment !

You had an error in your example: you were sending all OutputProps to the Component you were wrapping, but that was declared to only accept InputProps. So you need to desctructure the bar from props and send the rest of the props to Component.

Yeah but because I didn't use Exact objects for the Component props, it should accept useless props too, right ? But I think this still confuses Flow...
I really like your solution because Exact props is something I'd like too. I'm not used to rest object destructuring yet :)

Ah strangely when I make MyComponent props an exact object, I get the following error:

    27: <MyEnhancedComponent a={1} b={2} bar={42} />;
        ^ property `bar`. Property not found in
    20: class MyComponent extends React.Component<{|
                                                  ^ object type

despite the rest operation :)

BTW I see that otherProps is infered as {} by Flow. Not sure what to conclude from this :)

I have a similar issue I believe. Getting some incompatible instantiation of Props errors. I opened a stackoverflow post for it.

I'm wondering why this is causing errors:

import * as React from 'react';

function mergeTheme<Props: {}, T: {}>(
    Component: React.ComponentType<{ theme: T } & Props>,
    injectedTheme: T
): React.ComponentType<{ theme: T } & Props> {
    const Themed = (props: { theme: T } & Props) => {
        let theme: T = injectedTheme;
        return <Component {...props} theme={theme} />;
    };
    return Themed;
}

Really I want the returned HOC to be { theme?: $Shape<T> } & Props and then I would merge the themes. I managed to reduce that to the above code while still getting the same error.

This works but isn't correct and causes errors for consumers

function mergeTheme<Props: {}, T: {}>(
  Component: React.ComponentType<{ theme: T } & Props>,
  injectedTheme: T
): React.ComponentType<{ theme: T } & Props> {
  const Themed = (props: { theme: T } & Props) => {
      const baseProps: Props = props;
      let theme: T = injectedTheme;
      return <Component {...props} theme={theme} />;
  };
  return Themed;
}
Was this page helpful?
0 / 5 - 0 ratings