DEV Community

Wei Gao
Wei Gao

Posted on

Annotating Connected Components with Flow

After Flow 0.85, Flow starts Asking for Required Annotations on implicit calls of higher order components within each file import — export cycle. This facilitates Flow to merge type information from file dependencies before it walks the type structure and conducts type inference.

This helps Flow gain significantly better coverage on higher order components. But it also asks that we explicitly annotate the connected components at file exports. Exporting implicit calls of connect will raise error:

Missing type annotation for OP. OP is a type parameter declared in function type [1] and was implicitly
instantiated at call of connect [2].

In general, to make Flow happy with connect after 0.85 is a two-phase fix. First, you need to explicitly annotate each connected components at file exports. This shall clear all the “implicitly instantiated” errors. Then, if your codebase contains mismatched types between component definitions and usages, Flow will report those errors after you fix the implicit instantiation errors.

Fixing the “implicitly instantiated” errors at calls of connect

Note: We need React.AbstractComponent from Flow v0.89+

Annotating at function return

The easiest way to annotate connected components is to annotate at function call return. To do this, we need to know to types of props in our components:

  • OwnProps: likely contain or equal to what you need as the second parameter to mapStateToProps. If there are props that are not used by mapStateToProps, i.e., the props that "pass through", include them here in OwnProps as well
  • Props: OwnProps plus the props passed in by mapStateToProps and mapDispatchToProps

Note: Inexact objects don't spread nor $Diff very well. It is strongly recommended that you use exact objects for connected components all the time.

type OwnProps = {|
  passthrough: string,
  forMapStateToProps: string
|};

type Props = {|
  ...OwnProps,
  fromMapStateToProps: string,
  dispatch1: () => void
|};

With OwnProps and Props in figured out, we are now ready to annotate the connected components.

In component definition, annotate the props with Props. The component will have access to all the injected props from connect:

import * as React from "react";

const MyComponent = (props: Props) => (
  <div onClick={props.dispatch1}>
    {props.passthrough}
    {props.fromMapStateToProps}
  </div>
);

When we export, this is also when we normally call connect, annotate the exported component with just OwnProps:

import * as React from "react";

// const MyComponent = ...

export default (connect(
  mapStateToProps,
  mapDispatchToProps
)(MyComponent): React.AbstractComponent<OwnProps>);

Annotating by providing explicit type parameters

We may also annotate connected components by providing explicit type parameters at call of connect with the help of the newest Flow Typed library definition for React Redux. Note that this will also require Flow v0.89+.

The Flow Typed library definition declares connect as follows:

declare export function connect<-P, -OP, -SP, -DP, -S, -D>(
  mapStateToProps?: null | void,
  mapDispatchToProps?: null | void,
  mergeProps?: null | void,
  options?: ?Options<S, OP, {||}, MergeOP<OP, D>>
): Connector<P, OP, MergeOP<OP, D>>;

The libdef also contains a glossary of the abbreviations which decrypts the signature to:

connect<Props, OwnProps, StateProps, DispatchProps, State, Dispatch>()

For the most common ways to connect components, we won't need all of the parameters. Normally, we need only OwnProps and Props at the call of connect, and State at the definition of mapStateToProps.

We may use _ (what's this?) as placeholder at unused type parameter positions. A common connect call may look like this:

connect<Props, OwnProps, _, _, _, _>()

We include examples for three major use cases of annotating connect with Flow:

  • Connecting stateless component with mapStateToProps
  • Connecting components with mapDispatchToProps of action creators
  • Connecting components with mapStateToProps and mapDispatchToProps of action creators

Connecting stateless component with mapStateToProps

type OwnProps = {|
  passthrough: number,
  forMapStateToProps: string,
|};
type Props = {|
  ...OwnProps,
  fromStateToProps: string
|};
const Com = (props: Props) => <div>{props.passthrough} {props.fromStateToProps}</div>

type State = {a: number};
const mapStateToProps = (state: State, props: OwnProps) => {
  return {
    fromStateToProps: 'str' + state.a
  }
};

const Connected = connect<Props, OwnProps, _, _, _, _>(mapStateToProps)(Com);

Connecting components with mapDispatchToProps of action creators

type OwnProps = {|
  passthrough: number,
|};
type Props = {|
  ...OwnProps,
  dispatch1: (num: number) => void,
  dispatch2: () => void
|};
class Com extends React.Component<Props> {
  render() {
    return <div>{this.props.passthrough}</div>;
  }
}

const mapDispatchToProps = {
  dispatch1: (num: number) => {},
  dispatch2: () => {}
};
const Connected = connect<Props, OwnProps, _, _, _, _>(null, mapDispatchToProps)(Com);
e.push(Connected);
<Connected passthrough={123} />;

Connecting components with mapStateToProps and mapDispatchToProps of action creators

type OwnProps = {|
  passthrough: number,
  forMapStateToProps: string
|};
type Props = {|
  ...OwnProps,
  dispatch1: () => void,
  dispatch2: () => void,
  fromMapStateToProps: number
|};
class Com extends React.Component<Props> {
  render() {
    return <div>{this.props.passthrough}</div>;
  }
}
type State = {a: number}
type MapStateToPropsProps = {forMapStateToProps: string}
const mapStateToProps = (state: State, props: MapStateToPropsProps) => {
  return {
    fromMapStateToProps: state.a
  }
}
const mapDispatchToProps = {
  dispatch1: () => {},
  dispatch2: () => {}
};
const Connected = connect<Props, OwnProps, _, _, _, _>(mapStateToProps, mapDispatchToProps)(Com);

Annotating nested higher order components with connect

If you are at the unfortunate position where your component is wrapped with nested higher order component, it is probably more difficult to annotate by providing explicit type parameters, as doing so will probably require that you tediously take away props at each layer. It is agian easier to annotate at function return:

type OwnProps = {|
  passthrough: number,
  forMapStateToProps: string,
|}
type Props = {|
  ...OwnProps,
  injectedA: string,
  injectedB: string,
  fromMapStateToProps: string,
  dispatch1: (number) => void,
  dispatch2: () => void,
|}

const Component = (props: Props) => { // annotate the component with all props including injected props
  /** ... */
}

const mapStateToProps = (state: State, ownProps: OwnProps) => {
  return { fromMapStateToProps: 'str' + ownProps.forMapStateToProps },
}
const mapDispatchToProps = {
  dispatch1: number => {},
  dispatch2: () => {},
}

export default (compose(
  connect(mapStateToProps, mapDispatchToProps),
  withA,
  withB,
)(Component): React.AbstractComponent<OwnProps>)  // export the connected component without injected props

Benefits of this version

After fixing the implicit instantiation errors, if your code contains mismatched types between connected components, the total number of errors may go up. This is the result of Flow's improved coverage. If you are using console output for the Flow errors, you may not be able to see those errors until you clear other errors. These additional errors are grouped together, all tied back to React Redux's library definition, and have friendly error messages that will pin point you to the lines of code to the errors.

References

Articles

Upgrading guides

Talks

Others

Top comments (0)