All Articles

Typescript, react-redux and react-router-dom

Using TypeScript has many benefits. One of them is that it’s going to catch type incompatibilities quickly. You won’t be able to build the project if the TypeScript transpiler sees errors.

I am personally one of the people who when used TypeScript once, want to use it forever.

But TypeScript as every tools has its problems too. One of them is frustration that shows up when you either don’t know how to use TypeScript itself properly, or when you don’t know the internal typings of the library you’re using.

Most of the time I work with React to render UI and Redux to manage state of the apps. To make life simpler I use helper library for combining React and Redux together.

One problem that I’ve come across recently was how to have connected component and pass own props at the same time.

I had a class BookDetails

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { book } from '../../state/books/booksState';
import { CircularProgress } from '@material-ui/core';
import { appState } from '../../state/state';
import { loadBookDetails } from '../../effects/booksEffects';

interface Props {
  book: book;
  bookId: string;
  isLoaded: boolean;
  loadBook: (id: string) => void;
}

class BookDetails extends Component<Props> {
  componentDidMount() {
    if (!this.props.isLoaded) {
      this.props.loadBook(this.props.bookId);
    }
  }
  render() {
    const { isLoaded, book } = this.props;
    
    if (!isLoaded) {
      return <CircularProgress />;
    }

    return (
      <div>
        <h1>{book.title}</h1>
      </div>
    );
  }
}

const mapDispatchToProps = {
  loadBook: (id: string) => loadBookDetails(id)
}

const mapStateToProps = (state: appState, ownProps: Props) => {
  return {
    book: state.books[ownProps.bookId],
    isLoaded: state.books[ownProps.bookId] !== undefined
  }
}

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

But there was a problem when I wanted to use this class in other component Books:

import React, { Component } from 'react';

import BookDetails from '../BookDetails/BookDetails';

class Books extends Component {
  render() {
    return (
      <div>
        <BookDetails bookId={"123"}/>
      </div>
    )
  }
}

export default Books;

It complained that

Type ’{ bookId: string; }’ is missing the following properties from type ‘Props’: book, isLoaded, loadBook

Snipped of typescript code showing error that the component is missing required properties: book, isLoaded, loadBook

Then I came across this: https://react-redux.js.org/using-react-redux/static-typing

As it turned out, react-redux uses nice solution to automatically extract only the properties it needs so ultimately when using the component you need only to pass the own props. So the change our example here, we need to create three interfaces. One for the result of mapStateToProps, let’s call it StateProps; the result of mapDispatchToProps which defines our actions the component can dispatch, let’s call it DispatchProps; and finally our OwnProps. The last thing is to combine them together using intersection types.

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { book } from '../../state/books/booksState';
import { CircularProgress } from '@material-ui/core';
import { appState } from '../../state/state';
import { loadBookDetails } from '../../effects/booksEffects';

interface StateProps {
  book: book;
  isLoaded: boolean;
}

interface OwnProps {
  bookId: string;
}

interface DispatchProps {
  loadBook: (id: string) => void;
}

type Props = StateProps & DispatchProps & OwnProps;

class BookDetails extends Component<Props> {
  componentDidMount() {
    if (!this.props.isLoaded) {
      this.props.loadBook(this.props.match.bookId);
    }
  }
  render() {
    const { isLoaded, book } = this.props;
    
    if (!isLoaded) {
      return <CircularProgress />;
    }

    return (
      <div>
        <h1>{book.title}</h1>
      </div>
    );
  }
}

const mapDispatchToProps: DispatchProps = {
  loadBook: (id: string) => loadBookDetails(id)
}

const mapStateToProps = (state: appState, ownProps: OwnProps): StateProps => {
  return {
    book: state.books[ownProps.bookId],
    isLoaded: state.books[ownProps.bookId] !== undefined
  }
}

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

As you can see, now the Books component doesn’t complain about missing properties. book details no error .

This is possible because of the smart types definition that automatically skip the properties belongning to StateProps and DispatchProps

From node_modules/@types/react-redux/index.d.ts:

// Omit taken from https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html
export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;

export type InferableComponentEnhancerWithProps<TInjectedProps, TNeedsProps> =
    <C extends ComponentType<Matching<TInjectedProps, GetProps<C>>>>(
        component: C
    ) => ConnectedComponent<C, Omit<GetProps<C>, keyof Shared<TInjectedProps, GetProps<C>>> & TNeedsProps>;

// Infers prop type from component C
export type GetProps<C> = C extends ComponentType<infer P>
    ? C extends ComponentClass<P> ? ClassAttributes<C> & P : P
    : never;

Last thing I want to mention is how to use React Router props with all this. Let’s assume that the bookId is now passed as a property but instead it comes from the URL parameters.

To make it work we would need to extend OwnProps with RouteComponentProps and pass expected param types to the definition.

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { book } from '../../state/books/booksState';
import { CircularProgress } from '@material-ui/core';
import { appState } from '../../state/state';
import { loadBookDetails } from '../../effects/booksEffects';
import { RouteComponentProps, withRouter } from 'react-router-dom';

interface StateProps {
  book: book;
  isLoaded: boolean;
}

interface OwnProps extends RouteComponentProps<{ id: string }> {

}

interface DispatchProps {
  loadBook: (id: string) => void;
}

type Props = StateProps & DispatchProps & OwnProps;

class BookDetails extends Component<Props> {
  componentDidMount() {
    if (!this.props.isLoaded) {
      this.props.loadBook(this.props.match.params.id);
    }
  }
  render() {
    const { isLoaded, book } = this.props;
    
    if (!isLoaded) {
      return <CircularProgress />;
    }

    return (
      <div>
        <h1>{book.title}</h1>
      </div>
    );
  }
}

const mapDispatchToProps: DispatchProps = {
  loadBook: (id: string) => loadBookDetails(id)
}

const mapStateToProps = (state: appState, ownProps: OwnProps): StateProps => {
  return {
    book: state.books[ownProps.match.params.id],
    isLoaded: state.books[ownProps.match.params.id] !== undefined
  }
}

export default withRouter(
  connect(mapStateToProps, mapDispatchToProps)(BookDetails)
);

So this is how you can use TypeScript with react-redux. One things that becomes clear is that you need to produce a lot of boilerplate to make this work. One solution to this is to use hooks.

Each of the aforementioned libraries provide their hooks. So, taking the previous example we could do:

import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { book } from '../../state/books/booksState';
import { CircularProgress } from '@material-ui/core';
import { appState } from '../../state/state';
import { loadBookDetails } from '../../effects/booksEffects';
import { useParams } from 'react-router-dom';

const BookDetails = () => {
  const { id: bookId  } = useParams<{ id: string }>();
  const book = useSelector<appState, book>(state => state.books[bookId]);
  const isLoaded = book !== undefined;
  const dispatch = useDispatch();

  useEffect(() => {
    if (!isLoaded) {
      dispatch(loadBookDetails(bookId));
    }
  }, []);
  

  if (!isLoaded) {
    return <CircularProgress />;
  }

  return (
    <div>
      <h1>{book.title}</h1>
    </div>
  );
}

export default BookDetails;

As you can see it is much more concise, while still having Typescript powers.

Conclusion

If you want use TypeScript with react-redux, follow this guide: https://react-redux.js.org/using-react-redux/static-typing

If you can use React Hooks (React 16.8+), do it and it will save you a lot of boilerplate.