Top three React & TypeScript pitfalls

wojciechmatuszewski

Wojciech Matuszewski

Posted on June 2, 2021

Top three React & TypeScript pitfalls

The usage of React & TypeScript exploded in recent years. This should not come as a surprise to anyone at this point. Both tools proven to be viable working on web application large and small allowing developers to satisfy various business needs.

With the explosion of popularity comes also the explosion of mistakes that engineers can make while working with this stack in their day-to-day jobs. This blog aims to shed a light on my top three React & TypeScript pitfalls I’ve seen developers fall into and how they could be avoided.

Let us start with the most important one.

Using React.FunctionComponent or React.FC

I often see components being annotated as such:

import * as React from 'react'

type Props = {
    // ...
}

const FirstComponent = React.FC<Props> = (props) => {
    // ...
}

const SecondComponent = React.FunctionComponent<Props> = (props) => {
    // ...
}
Enter fullscreen mode Exit fullscreen mode

At first glance, it might seem like a good idea to type your components using these type abstractions. The ergonomics of React.FC and React.FunctionComponent might undoubtedly be tempting. By default, they provide you with typings for the children prop, the defaultProps, propTypes, and many other component properties.

You can read more about the React.FC and React.FunctionComponent here.

With all that being said, I believe that they introduce unnecessary complexity and are too permissive in terms of types.

Let us start with the most critical issue using either React.FC or React.FunctionComponent. I’m talking about the unnecessary complexity they introduce. Here is a simple question: which type of annotation feels more straightforward and easier to digest to you?

The one that where we explicitly annotate components arguments:

type Props = {
  // ...
};

const Component = (props: Props) => {
  // ...
};
Enter fullscreen mode Exit fullscreen mode

Or maybe the one where we use React.FC

import * as React from "react";

type Props = {
  // ...
};

const Component: React.FC<Props> = props => {
  // ...
};
Enter fullscreen mode Exit fullscreen mode

If you are familiar with React.FC, you might shrug your shoulders and say that both of them are completely valid options. And this is where the problem lies, mainly in the concept of familiarly or lack thereof.

The React.FC interface is shallow. In most cases, it can be replaced by annotating props explicitly. Now, imagine being new to a codebase, where React.FC is used extensively, but you have no idea what it means and what it does. You would most likely not be comfortable amending the Props type definitions within that codebase on your first day.

Another problem these typings introduce is the implicit composability by augmenting the Props definition with the children property.

I love how composable React components can be. Without the children property, it would be pretty hard to achieve one of my favorite patterns in React, the compound components pattern. With that in mind, I believe that we introduce misdirection to their APIs by making the composition of components implicit.

import * as React from "react";

const MarketingButton: React.FC<{}> = () => {
  // Notice that I'm not using `props.children`
  return <span>Our product is the best!</span>;
};

// In a completely separate part of the codebase, some engineer tries to use the `MarketingButton`.
const Component = () => {
  return <MarketingButton>HELLO!??</MarketingButton>;
};
Enter fullscreen mode Exit fullscreen mode

The engineer consuming the API would most likely be confused because, despite being able to pass the children in the form of a simple string, the change is not reflected in the UI. To understand what is going on, they would have to read the definition of the MarketingButton component - this is very unfortunate. It might seem like a contrived example, but imagine all the seconds lost by thousands of engineers each day going through what I’ve just described. This number adds up!

Typing the children property wrong

In the last section, I touched on how important the children prop is. It is then crucial to correctly annotate this property to make other developer’s work with life easier.

I personally have a simple rule that I follow that works for me:

Use React.ReactNode by default. Change if necessary

Here is an example

type Props = {
  children: React.ReactNode;
};

const MarketingButton = ({ children }) => {
  return <button>{children}</button>;
};
Enter fullscreen mode Exit fullscreen mode

I find myself opting out of React.ReactNode very rarely, primarily to further constrain the values of the children prop. You can find a great resource to help you pick what type of the children prop you should use here.

Leaking component types

How often do you encounter a component written in a following way:

export type MyComponentProps = {
  // ...
};

export const MyComponent = (props: MyComponentProps) => {
  // ...
};

// Some other part of the codebase, possibly a test file.
import { MyComponentProps } from "../MyComponent";
Enter fullscreen mode Exit fullscreen mode

Exporting the MyComponentProps creates two problems.

  1. You have to come up with a name for the type. Otherwise, you will end up with a bunch of exported symbols that all have the same name. Operating in such a codebase is cumbersome because you have to actively pay attention to where the auto-completion imports the symbols from.
  2. It might create implicit dependencies that other engineers on your team might not be aware of.
    • Can I change the name of the type?
    • Is MyComponentProps type used somewhere else?

Whenever you keep the type of the props un-exported, you avoid those issues.

There exists a mechanism that allows you to extract the type of props for a given component without you having to use the export keyword. I’m referring to React.ComponentProps generic type. The usage is as follows.

type Props = {
  // ...
};

export const MyComponent = (props: Props) => {
  // ...
};

// In another file.
import { MyComponent } from "../MyComponent";
type MyComponentProps = React.ComponentProps<typeof MyComponent>;
Enter fullscreen mode Exit fullscreen mode

I’ve been using this technique for the past two years that I’ve been writing React & TypeScript code, and I have never looked back. You can read more about how useful this generic type is in the context of writing component tests in one of my other blog posts.

Summary

These were the top three pitfalls I’ve most commonly seen in the wild. I hope that you found my ramblings helpful.

If you noticed that something I’ve written is incorrect or would like to clarify a part of the article, please reach out!

You can find me on twitter - @wm_matuszewski

Thank you for your time.

💖 💪 🙅 🚩
wojciechmatuszewski
Wojciech Matuszewski

Posted on June 2, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related