Flow: Cannot properly type Props for Higher Order React components

Created on 31 Mar 2016  路  12Comments  路  Source: facebook/flow

Given the following wrapper react component

class Wrapper extends React.Component {
   constructor(props) {
        super(props);
   }
   onChangeHandler() {
   }
   render() {
       return React.cloneElement(this.props.children, {onChange: this.onChangeHandler});
   }
}

and the following wrapped component

type CompProps = {
   onChange: ():void
}

class WrappedComp extends React.Component<void, CompProps, void> {
   render() {
      return <div onClick={this.props.onChange}></div>
  } 
}

When the following hierarchy is being rendered:

<Wrapper>
    <WrappedComp />
</Wrapper>

Flow 0.22.1 keeps on complaining about missing onChange prop.

We have loads of patterns very similar to this in our projects.

Is there a way to have Flow recognize that the onChange prop is being passed by means of React.cloneElement and not directly as component prop ?
Making the onChange nullable is not an option as it would force lots of checks all over the place for the props existance when this is not really an issue in reality.

Most helpful comment

UPDATE: Good News. Found a solution that actually works.
Instead of explaining here's an example with the errors

/* @flow */
import React from 'react'

type Props = {abacus: string, bear: number}

class Test extends React.Component<void, Props, void> {
  render () {
    return <div>Hello</div>
  }
}

declare function hoc<D, P, S, C: React$Component<D, P, S>>(
  component: Class<C>,
  config: any
): Class<React$Component<D, $Diff<P, {bear: number}>, S>>

const HOCTest = hoc(Test, {})

// This works correctly, it knows you don't need `bear`
const y = <HOCTest abacus='sd' />

// These are errors
const y = <HOCTest /> // missing prop
const y = <HOCTest abacus={12} /> // incorrect type for abacus

So, this solution needs one surpassing comment, but is overall the best solution I've found.
EDIT: fixed.

It actually type checks the components correctly.

All 12 comments

Related: #1523

I think this issue should be closed in favor of #1523

I found a fix. Caveat: It needs the undocumented magic type $Diff.

Just put something like this in a .flow file next to your HOC file.

declare function inject<Config> (
  dependencies: any,
  Komponent: ReactClass<Config>
): ReactClass<$Diff<Config, InjectedProps>>

module.exports = inject

Update: React doesn't seem to work very well with $Diff is most cases.

When writing an object as in:

const obj: $Diff<A, B> = { ... }

It works correctly. But in all other cases, it doesn't seem to be working at all.

UPDATE: Good News. Found a solution that actually works.
Instead of explaining here's an example with the errors

/* @flow */
import React from 'react'

type Props = {abacus: string, bear: number}

class Test extends React.Component<void, Props, void> {
  render () {
    return <div>Hello</div>
  }
}

declare function hoc<D, P, S, C: React$Component<D, P, S>>(
  component: Class<C>,
  config: any
): Class<React$Component<D, $Diff<P, {bear: number}>, S>>

const HOCTest = hoc(Test, {})

// This works correctly, it knows you don't need `bear`
const y = <HOCTest abacus='sd' />

// These are errors
const y = <HOCTest /> // missing prop
const y = <HOCTest abacus={12} /> // incorrect type for abacus

So, this solution needs one surpassing comment, but is overall the best solution I've found.
EDIT: fixed.

It actually type checks the components correctly.

@nmn Did you mean bear: number where it says bb: number ?

@namuol Thanks! fixed.

Unfortunately this doesn't work with functional components and looking at https://github.com/facebook/flow/commit/a66c76453fb105bbaa250874a6d65c70d08300c8 it seems it's impossible to express the type for React components (class or functional component) in "userland".

@andreypopp It CAN be made to work for functional components:

/* @flow */
import React from 'react'

type Props = {abacus: string, bear: number}

class Test extends React.Component<void, Props, void> {
  render () {
    return <div>Hello</div>
  }
}

type Hoc = <D, P, S, C: React$Component<D, P, S>>(
  component: Class<C>,
  config: any
): Class<React$Component<D, $Diff<P, {bear: number}>, S>> &
<P>(
  component: (props: P) => any,
  config: any
): Class<React$Component<void, $Diff<P, {bear: number}>, void>>

declare var hoc: Hoc

const HOCTest = hoc(Test, {})

// This works correctly, it knows you don't need `bear`
const y = <HOCTest abacus='sd' />

// These are errors
const y = <HOCTest /> // missing prop
const y = <HOCTest abacus={12} /> // incorrect type for abacus

So essentially just overloading the type of the HOC to accept a function instead of a React$Component.
Bonus: This works with flow 0.24-0.25. Which don't otherwise type check functional components.

I'll take a look at flow 0.26 and see if something needs to be changed to make it work correctly there.

Note: Your function component will have to be typed correctly to make default props optional as functional components don't come with some special object to declare default props.

@nmn thanks, will try it, I tried to do a union of class and function component types but it is clear to me now why you do intersection here instead.

@nmn @samwgoldman This is a different case than the HOC in #1523; that example uses a decorator function, whereas this uses a wrapper component that injects props with cloneElement. There is nothing in the docs about wrapper-style HOCs like the example in the OP is there?

@jedwards1211 You're absolutely right, the above solutions don't actually work for the OP case. I took a crack at it tonight, and came up short. But Maybe someone could point me in the right direction?

type Props = {
  foo: string,
  bar: string,
};

type DefaultProps = {
  foo: string,
};

declare class React2$Element<Config, DP> extends React$Element{
  type: _ReactClass<DP, *, Config, *>;
}


declare function Hoc<Config, DP: DefaultProps, R: React$Element<Config>>(props: {children: R}) : React2$Element<Config, DP>

function TestComponent({foo, bar}: Props){
  return <div>{bar}</div>;
}


function Hoc(props){
  return React.cloneElement(props.children, {foo: 'form2wr'});
}


function Test(){
  return <Hoc children={<TestComponent bar='yo' />}></Hoc>;
}

Technically, this is cheating because I'm replacing the default props with the hoc props, but I'm willing to sacrifice default props if it gives me a solution here.

This is all based off of https://github.com/facebook/flow/blob/v0.29.0/lib/react.js#L121

Was this page helpful?
0 / 5 - 0 ratings