Enforcing Method Presence in React Components with Flow

By Mark Siebert on November, 6 2017

enforcing-method-presence-react-components-flow

Recently I was on Flow’s Gitter when someone asked the following question:

How can I type that I want a function argument to be a React component with fooBar static method on it?

This question made me remember that a couple months ago, I’d had a similar problem (albeit without the static requirement). I’ll explain how to ensure the method is present when composing higher order components, but expect that you know how to type generic HOCs as described in the Flow documentation.

Essentially, the main idea is to create an interface that contains the method you want to have and join it with the component passed into the function that generates the HOC.

interface Enforced {
  enforcedMethod(): void;
}

function<Props, State>(
    component: Class<React.Component<Props, State> & Enforced>
): Class<React.Component<Props, State> { ... }

Basically, this says, “Give me a class that’s a React component that also has an instance method called enforcedMethod and I’ll give you back a React component with the same props and state types.”

As a more complete example, suppose we wanted to build a higher-order component that fired the onClickedOutside method of a component when the user clicks anywhere outside the resulting element. It would be pretty catastrophic if we applied our HOC to a component that didn’t have an onClickedOutside, so we’ll make Flow warn us if it isn’t present.

import * as React from 'react'
import ReactDOM from 'react-dom'

interface HasClickOutside {
  onClickedOutside(e: Event): void;
}

function ClickOutside<Props, State>(
    Wrappable: Class<React.Component<Props, State> & HasClickOutside>
): Class<React.Component<Props, State>> {
  return class extends Wrappable {
    componentDidMount = (): void => {
      document.addEventListener(
        'click',
        this.outsideClickHandler,
        true
      )
    }

    componentWillUnmount = (): void => {
      document.removeEventListener(
        'click',
        this.outsideClickHandler,
        true
      )
    }

    outsideClickHandler = (e: Event): void => {
      const domNode = ReactDOM.findDOMNode(this);
      const clickedOutside = !domNode ||
        ((e.target instanceof Node) &&
          !domNode.contains(e.target))
      if (clickedOutside) {
        this.onClickedOutside(e)
      }
    }
  }
}

// No errors!
ClickOutside(class extends React.Component<{}> {
  onClickOutside = (e: Event): void => { ... }
})

// Error: property `clickedOutside` of HasClickOutside.
// Property not found in ...
ClickOutside(class extends React.Component<{}> {
  // no onClickOutside method
})

With this HOC, we can make a component set something within its state or take some other action when we click outside, and Flow will warn us if we don’t tell the HOC what to do when we click outside the element!

Now, let’s turn back to the original question of how to enforce a static method on a component. One would think that it would be as simple as adding static to the interface, but you and I know better. Luckily, all we need to do is tell Flow that our function expects a class that is a React component that has a callable signature.

interface Enforced {
  enforcedMethod(): void;
}

function<Props, State>(
    component: Class<React.Component<Props, State>> & Enforced
): Class<React.Component<Props, State> { ... }

You’ll note that all we had to change from our first example was moving the type union outside of the Class<...> expression. Now Flow will enforce that we pass in something with a static enforceMethod!

If you need a suite cloud monitoring tools that install quickly and integrate was AWS services, you should take a look at what we are working on.

LEARN MORE