4 Comments

Typesafe Container Components with React-Redux’s Connect and TypeScript

TypeScript is fantastic—you get feedback as you write your code to (mostly) ensure you’re passing valid arguments to functions and props to components. However, when using the connect function, you may find that you are permitted to pass invalid props or omit required props to components. In this post, I’ll discuss how the issue arises, and how to fix it by typing your calls to connect!

Setting Up the Component

If you are using TypeScript and React, you more than likely provide types for your component props. It’s easy to do, and it ensures that your components are being provided the right props. Here’s a simple example:



interface MyProps {
  label: string;
  clickCount: number;
  handleClick: () => void;
}

export default class MyComponent extends React.Component {
  render() {
    const { label, clickCount, handleClick } = this.props;
    return (
      <div>
        <div onClick={handleClick}>
          {label}
        </div>
        <div>
          Clicks: {clickCount}
        </div>
      </div>
    );
  }
}

If you are getting your data from a Redux store, you may wrap this presentation component in a container component generated by connect. I’m going to do some hand-waving and assume we’ve already defined types for the Redux store’s state(if not, check the addition at the bottom of the post) and set up our actions and reducers. Given that, we could create the following container as follows:



const mapStateToProps = (state: State) => ({
  clickCount: state.myData.clickCount
});

const mapDispatchToProps = (dispatch: Dispatch) => ({
  handleClick: () => dispatch(incrementCounterAction())
});

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(MyComponent);

Then elsewhere in the codebase, we could bring this in using:



<MyContainer />

It Looks Good, But…

At this point, your text editor should show no errors, and your code should happily transpile. There’s just one problem: The label prop is never passed to the presentation component!

Assuming we expect the label to be provided by the container, I should have added another line in mapStateToProps so it looked like this:



const mapStateToProps = (state: State) => ({
  label: state.myData.label,
  clickCount: state.myData.clickCount
});

Unfortunately, TypeScript does not catch this error. We can remedy this, though, by providing connect with types.

Typing Connect

As a starting point, I’ll add types to mapStateToProps and mapDispatchToProps.



interface StateFromProps {
  label: string;
  clickCount: number;
}

interface DispatchFromProps {
  handleClick: () => void;
}

The next step is to add types to connect. The connect accepts two arguments, mapStateToProps and mapDispatchToProps, returns a function that accepts a Component type, and returns another component. We need to provide types for the two arguments, and the props of the component that will be returned. For the time being, I’ll just add types for mapStateToProps and mapDispatchToProps.



export default connect<StateFromProps, DispatchFromProps, void>(
  mapStateToProps,
  mapDispatchToProps
)(MyComponent);

With this in place, TypeScript will recognize errors if you do not return the correct data types from mapStateToProps and mapDispatchToProps.

Passing Props to the Container

In the example above, the third type I gave to connect was void, indicating no props needed to be passed to the component it generates. Let’s say, though, that you would like to pass label through the container, rather than get it from the store. To accomplish that, just remove the label from mapStateToProps (reverting back to its original version). And instead of void, pass the type { label: string }.

We now have something like this:



const mapStateToProps = (state: State) => ({
  clickCount: state.myData.clickCount
});

const mapDispatchToProps = (dispatch: Dispatch): DispatchFromProps => ({
  handleClick: () => dispatch(incrementCounterAction())
});

export default connect<StateFromProps, DispatchFromProps, { label: string }>(
  mapStateToProps,
  mapDispatchToProps
)(MyComponent);

And we can use it like this:



<MyContainer label={'It works!'} />

There you have it—typesafe container components generated through connect.

Typing the Store and Dispatch

To revisit the above hand-waving on typing the store, here’s how you could type the store:



export type Store = {
  myData: {
    clickCount: number,
    label: string
  }
}

And dispatch with actions:



type ActionA = {
  type: 'INCREMENT_COUNTER'
}
type Action = ActionTypeA | OtherAction;
export type Dispatch = (action: Action) => void;