DEV Community

loading...
Cover image for Top three React & TypeScript pitfalls

Top three React & TypeScript pitfalls

Wojciech Matuszewski
Doing stuff with JavaScript / Golang. Probably coding / working or at the gym.
・4 min read

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.

Discussion (11)

Collapse
psiho profile image
Mirko Vukušić • Edited

Last point is nice. Will use that. FC.... Actually, I find FC more readable. Since day one when I tried both ways. Don't know why, maybe because somehow it clearly emphasizes it is an React component. As for issue with Children, often I use Omit and find that also very readable exception to the usual case where children are accepted.

Collapse
ypresto profile image
Yuya Tanaka

While I agree with keeping props type un-exported where possible, I don't recommend ComponentProps.

  • It is quite complicated type (see its declaration) and it might hit performance of type check.
  • Generics is not supported with typeof, and it cause weird type error if the component has generic props. (i.e. you can't ComponentProps<typeof FooComponent<T>>)
  • Explicit dependency to props type helps to find reference and to decouple (props) type from (component) implementation.

You can also use React.VFC to omit children from React.FC and annotate type correctly.
(I personally use function Foo() { ... } as it also supports overload when it is absolutely necessary.)

Collapse
psiho profile image
Mirko Vukušić

Uh, React.VFC! Good one. Didn't know that. Thx.

Collapse
sqlrob profile image
Robert Myers

Wouldn't that performance hit only be on compile?

Collapse
sgolovine profile image
Sunny Golovine

I disagree with the point you made about not using React.FC. One big reason I always use it now is that it gives you the children prop for free. So my code went from looking like this:


const MyComponent = ({ children} : {children: ReactNode}) => {...}

Enter fullscreen mode Exit fullscreen mode

to just this


const MyComponent: React.FC = ({ children }) => {....}

Enter fullscreen mode Exit fullscreen mode

Small change but accross hundreds of components it starts making a difference.

Collapse
0916dhkim profile image
Donghyeon Kim

I did not know about React.ComponentProps type. Thanks! And yes, dealing with children prop is difficult, especially when it is defined inside a third-party library.

Collapse
mremanuel profile image
Emanuel Lindström • Edited

Good, informative post. Well written.
The last line of code was especially valuable. I will definitely use it in the future!
type MyComponentProps = React.ComponentProps<typeof MyComponent>

Collapse
jfbrennan profile image
Jordan Brennan

The fourth pitfall is using either of these in the first place 😜
I couldn't be more done with over-engineered, non-standard, dependency-laden stuff like React and TS.

Collapse
devinrhode2 profile image
Devin Rhode

RE: "Is MyComponentProps type used somewhere else?"

In vscode, you can right click exported variables and select "Find all references"

Collapse
wojciechmatuszewski profile image
Wojciech Matuszewski Author

Thank you for reaching out.

The problem with your approach is that it's optimizing for a local maximum - your environment.

The goal of writing code should be to optimize for a global maximum - readability and precision in every situation and every circumstance. That is why I think it's essential to take a step back and not think about what my editor can do and communicate the intent with the way code is structured and written in a given file.

Exporting things in JavaScript makes it unclear (by just looking at the code) what kind of other parts of the codebase use that exported symbol.
This uncertainty can be dangerous, albeit mostly in extreme cases, but I believe it's still something we should watch out for.

Collapse
rodelta profile image
Ro De la Rivera

To replace FC<> you need to also type the response type of the function and that involves more (and repeated) code.