I was slow to embrace TypeScript. Not because I think TypeScript is bad, but
more for my love of the dynamism of JavaScript. Surprisingly, I find myself
looking back at the past projects and they've all been in TypeScript. I think it
might be time to face the truth: I mostly code in TypeScript. But in my React
projects, there has always been one thing bothering me: React.FC
.
For some reason when I was learning TypeScript and React almost all blog posts,
tutorials and examples were using React.FC
to type up function components. And
it makes sense in a way. You have a built-in type in the DefinitlyTyped React
types that says React.FunctionComponent
. Seems like a perfect fit! But alas, I
think it is the wrong path to take and has some obvious drawbacks. I suggest
typing properties directly as a function parameter instead of the React.FC
helper type:
type MyComponentProps = { title: string };
// 👍 Like this..
function MyComponentFC(props: MyComponentProps) {}
// 👎 ...not like this
const MyComponent: React.FC<MyComponentProps> = (props) => {};
Let's investigate why!
If you want to use React.FC
as a type you have to annotate the function type
itself. Not the parameters or the return value, but the function value. This
means you need to have it as an expression. Either as an
arrow function or as a regular function expression.
I like function expressions, but usually prefer function declarations for
top-level functions. There are a couple of reasons why: Prioritizing content
through hoisting and direct exports.
When creating JavaScript/TypeScript modules I try to emphasize what is the most important content. Preferably, I try to optimize so that relevant content for understanding context and the main goal of the module is shown "above the fold" in your file. Meaning, when opening it in an editor or code explorer you see most of the important content. This means the main exported component with helper components and utils moved further down:
// 1. Setup variables. Works as expected
const BoundFooItem = partial(FooItem, { title: "Foo" });
// 2. Main export
export default function Foo() {}
// 3. Additional exports. Often children types that can be semantically grouped with main export.
// (Many prefer not to mix default with named exports, but a discussion for another time).
export function FooItem() {}
// 4. Helper components
function MyInternalComponent() {}
// 5. Utils
function add() {}
function partial() {}
function identity() {}
For this to work both FooItem
and MyInternalComponent
need to be hoist-able.
Using function expression allows this type of hoisting under the right
circumstances, but can also trigger Temporal Dead Zone (TDZ). Function
declarations are a safer choice.
const BoundFooItem = partial(FooItem, { title: "Foo" });
// OOOPS: ReferenceError: Cannot access 'FooItem' before initialisation
export default function Foo() {}
export const FooItem = () => {};
If you've ever worked with a codebase using React.FC
you'll probably recognize
this pattern:
const MyComponent: React.FC<{}> = () => {
// Potentially lots of content here...
// ...
};
export default MyComponent;
This is due to a combination of TypeScript and module grammar not supporting
typed function expressions as the default export. This is more subjective, but I
like grouping exporting and creation so it's easy to track what is the module's
public API. Using default export with declaration doesn't support using
React.FC
.
// Clearer that MyComponent is the main API.
export default function MyComponent() {}
Lack of hoisting without TDZ and not being able to export components directly
are limitations we can work around. But there is one technical limitation that I
struggled with when learning TypeScript with React and that makes it hard to
recommend using React.FC
as a general-purpose typing style for React
Components: Generics.
There are often cases when you create reusable components like drop-downs or
lists where you don't exactly know what type the passed items are. This is the
case for generics
. React and TypeScript supports generics, but the grammar of
TypeScript does not allow you to do generics when using React.FC
:
type MyDropDownProps<T> = {
items: T[];
itemToString(item: T): string;
onSelected(item: T): void;
};
// Neither of these examples are valid
const MyDropDown: React.FC<MyDropDownProps> = (props) => {};
const MyDropDown: React.FC<MyDropDownProps<T>> = <T>(props) => {};
const MyDropDown: React.FC<MyDropDownProps<T>> = (props) => {};
const MyDropDown<T>: React.FC<MyDropDownProps<T>> = (props) => {};
// How?? No direct way when using React.FC
To properly use generics with MyDropDown
we would have to use types directly
as typed parameters:
type MyDropDownProps<T> = {
items: T[];
itemToString(item: T): string;
onSelected(item: T): void;
};
// Valid code
function MyDropDown<T>(props: MyDropDownProps<T>) {}
By typing parameters directly we use TypeScript the same way as we would do with any other function. No special case for React components. This means we can use our existing knowledge from other codebases with TypeScript. The one thing we lack here is typing the returned value from our component. No worries, though! TypeScript is more than capable of inferring the return type on its own. In the cases where you need to be explicit, you can add return type manually:
function MyDropDown<T>(props: MyDropDownProps<T>): JSX.Element {}
Talking about JSX.Elements. If we use the proposed solution from above, how do we handle children? Because this won't work:
type MyComponentProps = { className: string };
// Does not work.
function MyComponent({ className, children }: MyComponentProps) {
// children is not typed and TypeScript complains
return <div className={className}>{children}</div>;
}
But using React.FC
works with children:
// Works as expected:
const MyComponent: React.FC<MyComponentProps> = function ({
className,
children,
}) {
// children is typed
return <div className={className}>{children}</div>;
};
This is because React.FC
adds children
property to the props type. This
seems like a convenience, but it is actually a bad thing! By using React.FC
it
always looks like you take children as properties:
type MyComponentProps = { title: string };
const MyComponent: React.FC<MyComponentProps> = function ({ title }) {
// children is not typed and TypeScript complains
return <div>{title}</div>;
};
// TypeScript Allows this, but it is false positive in a sense.
const myValue = <MyComponent title="Hello">My children</MyComponent>;
// This is more correct
function MyComponentCorrect({ title }: MyComponentProps) {
return <div>{title}</div>;
}
const myValueCorrect = (
<MyComponentCorrect title="Hello">My children</MyComponentCorrect>
);
// Error:
// Type '{ children: string; title: string; }' is not assignable to type 'IntrinsicAttributes & MyComponentProps'.
// Property 'children' does not exist on type 'IntrinsicAttributes & MyComponentProps'.
Using prop types directly on the parameter offers more flexibility and correctness without allowing false positives. But how do you type children, then? You can use two different ways: either manually or using helper types from React.
// Manually
type MyComponentProps = {
title: string;
// ReactNode is all allowed children types including arrays, fragments, scalar values, etc.
children: React.ReactNode;
};
function MyComponentCorrect({ title, children }: MyComponentProps) {}
If you find it difficult remembering what type to use for children you can use a convenience helper from the React types:
type MyComponentProps = React.PropsWithChildren<{
title: string;
}>;
function MyComponentCorrect({ title, children }: MyComponentProps) {}
Which does the same thing. PropsWithChildren
is defined as an intersection
type, much like React.FC
:
type PropsWithChildren<P> = P & { children?: ReactNode };
// (note: optional type is redundant as ReactNode can be undefined)
Using props directly on parameters allow you to more correctly type components and avoid false positives while also being more flexible.
Starting up with TypeScript and React the React.FC
was confusing to me. And
I'm honestly not sure why it is so widely used. I think newcomers should see
established idiomatic practices of TypeScript, not odd practices tied to
specific libraries. The default practice should allow for common features such
as generics without being confusing.
I suggest for being consistent in typing, better allowing for hoisting and
direct default exports, avoiding false positives for children and support for
generics, to always type React component props directly as function parameters.
This way you can use TypeScript as with any other function and also be more
correct than when using React.FC
. As an added benefit, you can easier switch
between component-based libraries such as preact
(et.al.).
When using typed parameters instead of React.FC
, you might have to explicitly
type return values as JSX.Element
, but in almost all practical cases this is
something TypeScript can infer. Some might also say a drawback can be having to
explicitly type the parameters, making function signature harder to read. I
don't agree, but I can see how some people think so. I think the pros massively
outweigh the few cons.
This isn't a
new
opinion, but still, I
see a lot of codebases and tutorials where React.FC
is the de-facto standard.
So I think it is a message that that bears repeating. Is it, though, worth
rewriting/codemodding your entire codebase? Probably not, but I think it's
definitely worth using parameter types directly for all new components here on
out and gradually removing React.FC
from your codebase.