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) => {
// ...
}
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) => {
// ...
};
Or maybe the one where we use React.FC
import * as React from "react";
type Props = {
// ...
};
const Component: React.FC<Props> = props => {
// ...
};
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>;
};
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>;
};
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";
Exporting the MyComponentProps
creates two problems.
- 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.
- 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>;
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.
Top comments (10)
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.
While I agree with keeping props type un-exported where possible, I don't recommend
ComponentProps
.typeof
, and it cause weird type error if the component has generic props. (i.e. you can'tComponentProps<typeof FooComponent<T>>
)You can also use
React.VFC
to omit children fromReact.FC
and annotate type correctly.(I personally use
function Foo() { ... }
as it also supports overload when it is absolutely necessary.)Wouldn't that performance hit only be on compile?
Uh, React.VFC! Good one. Didn't know that. Thx.
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:
to just this
Small change but accross hundreds of components it starts making a difference.
I did not know about
React.ComponentProps
type. Thanks! And yes, dealing withchildren
prop is difficult, especially when it is defined inside a third-party library.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>
RE: "Is MyComponentProps type used somewhere else?"
In vscode, you can right click exported variables and select "Find all references"
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.
To replace FC<> you need to also type the response type of the function and that involves more (and repeated) code.