React.FC
has had a controversial reputation, often seen as an outdated or limiting way to type components. For years, developers criticized it for its implicit behavior with children and certain typing constraints.
But recent updates in TypeScript and React mean React.FC
has now overcome its main weaknesses, making it a strong choice for typing components. Here’s why it’s worth reconsidering.
With these updates, React.FC
now enforces more accurate return types and provides improved error handling within components, alongside compatibility with defaultProps
, contextTypes
, and displayName
. I'll break down why this shift is significant, compare the main typing approaches, and offer a summary that may surprise those who’ve written off React.FC
in the past.
Table of Contents:
-
1. What is
React.FC
- 2. Old
React.FC
issues - 3. The different approaches of typing React components
- 4. Conclusion
1. What is React.FC
React.FC
is a TypeScript type for defining functional components in React. It ensures your component returns a ReactNode
and provides a built-in way to type props
. With React.FC
, you can confidently handle component typing without worrying about return type issues — including support for elements, numbers, strings, and undefined
.
You don’t have to define FC
yourself; it’s available from React’s typings:
interface FC<P = {}> {
(props: P, context?: any): ReactNode;
propTypes?: WeakValidationMap<P> | undefined;
contextTypes?: ValidationMap<any> | undefined;
defaultProps?: Partial<P> | undefined;
displayName?: string | undefined;
}
1.1. How to Use React.FC
Here’s how React.FC
is used to type components. It enforces that the component returns a renderable item and supports prop typing.
interface FooProps {
bar: string;
}
const Foo: React.FC<FooProps> = ({ bar }) => {
return <div>{bar}</div>;
};
You can also use React.FC
for components without props:
const Foo: React.FC = () => {
return <div>Hello World</div>;
};
This type ensures the return type is a ReactNode
, offering advantages in typing and error handling that we’ll explore further.
2. Old React.FC
issues
In the past, React.FC
posed some significant challenges for developers, including:
-
Implicit children prop:
React.FC
previously added an implicitchildren
prop, even if the component didn’t need it. This could lead to unexpected behavior and errors. -
DefaultProps compatibility:
React.FC
had issues withdefaultProps
, making it harder to set default values for component props reliably. -
Limited return types: It didn’t support returning
undefined
,string
, ornumber
, which limited some common use cases. -
No support for generics: While still unsupported, this limitation is due to TypeScript’s handling of generics, not
React.FC
itself.
Since these issues were addressed, React.FC
has become much more flexible, allowing most common use cases without workarounds. For an in-depth explanation of these fixes, see Matt Pocock | Since TypeScript 5.1, React.FC is now "fine", a thorough resource from an authoritative TypeScript expert.
3. The different approaches of typing React components
3.1. Using Explicit React.FC
Typing
Why enforce the return type? I like to enforce it because this function should return a valid ReactNode
and if it didn't it should fail inside the component so that I can see it and fix it right away.
This saved me countless times from forgetting to return anything in the component for example.
3.1.1. Valid return types examples
const FooUndefined: React.FC = () => {
return undefined; // Doesn't complain about returning undefined
};
const FooString: React.FC = () => {
return "Hello World"; // Doesn't complain about returning a string
};
const FooNumber: React.FC = () => {
return 123; // Doesn't complain about returning a number
};
const FooJSX: React.FC = () => {
// Doesn't complain about returning a JSX.Element
return {
type: "div",
children: "Hello, world!",
key: "foo",
props: {},
};
};
const Foo: React.FC = () => {
// Doesn't complain about returning a JSX markup that gets transpiled to JSX.Element under the hood
return <div>Hello World</div>;
};
const Bar: React.FC = () => {
return (
<>
<FooUndefined /> {/* Doesn't complain about using FooUndefined */}
<FooString /> {/* Doesn't complain about using FooString */}
<FooNumber /> {/* Doesn't complain about using FooNumber */}
<FooJSX /> {/* Doesn't complain about using FooJSX */}
<Foo /> {/* Doesn't complain about using Foo */}
</>
);
};
3.1.2. Invalid return types examples
const FooVoid: React.FC = () => {
// ! ERROR: Type '() => void' is not assignable to type 'FC<{}>'.
return;
};
const FooInvalid: React.FC = () => {
// ERROR: Type '() => { invalid: string; }' is not assignable to type 'FC<{}>'.
return {
invalid: "object",
};
};
const Bar: React.FC = () => {
return (
<>
<FooVoid /> {/* Doesn't complain about using FooVoid */}
<FooInvalid /> {/* Doesn't complain about using FooInvalid */}
</>
);
};
3.1.3. Pros and Cons
Pros:
- ✔️ Enforces the return type of the component to be a valid
ReactNode
so you can't return invalid types. - ✔️ Throws the error inside the faulty component itself so you can see the problem right away and fix it immediately.
- ✔️ Supports returning numbers, strings, and
undefined
. - ✔️ Supports
propTypes
,contextTypes
,defaultProps
, anddisplayName
.
Cons:
- ❌ Doesn't support generics.
- ❌ Doesn't support function declaration while writing a component, only supports function expressions.
- ❌ Code is a little bit more verbose.
3.2. Infer Return Type
Now let's try inferring the types while returning invalid renderable value types.
3.2.1. Valid return types examples
// Inferred as () => undefined
const FooUndefined = () => {
return undefined; // Doesn't complain about returning undefined
};
// Inferred as () => string
const FooString = () => {
return "Hello World"; // Doesn't complain about returning a string
};
// Inferred as () => number
const FooNumber = () => {
return 123; // Doesn't complain about returning a number
};
// Inferred as () => { type: string; children: string; key: string; props: {}; }
const FooJSX = () => {
// Doesn't complain about returning a JSX.Element
return {
type: "div",
children: "Hello, world!",
key: "foo",
props: {},
};
};
// Inferred as () => JSX.Element
const Foo = () => {
// Doesn't complain about returning a JSX markup that gets transpiled to JSX.Element under the hood
return <div>Hello World</div>;
};
const Bar = () => {
return (
<>
<FooUndefined /> {/* Doesn't complain about using FooUndefined */}
<FooString /> {/* Doesn't complain about using FooString */}
<FooNumber /> {/* Doesn't complain about using FooNumber */}
<FooJSX /> {/* Doesn't complain about using FooJSX */}
<Foo /> {/* Doesn't complain about using Foo */}
</>
);
};
3.2.2. Invalid return types examples
// Inferred as '() => void'
const FooVoid = () => {
return; // Doesn't complain about returning nothing.
};
// Inferred as '() => { invalid: string; }'
const FooInvalid = () => {
// Doesn't complain about returning an invalid object
return {
invalid: "object",
};
};
const Bar = () => {
return (
<>
<FooVoid /> {/* <-- ! ERROR: 'Foo' cannot be used as a JSX component. */}
<FooInvalid /> {/* <-- ! ERROR: 'Foo' cannot be used as a JSX component. */}
</>
);
};
3.2.3. Pros and Cons
Pros:
- ✔️ Supports both function declaration and expression while writing a component.
- ✔️ Supports type generics.
- ✔️ Code is less verbose.
Cons:
- ❌ Doesn't enforce the return type of the component to be a valid
ReactNode
so you can return invalid types. - ❌ Throws the error in the parent component that's using the faulty component so you have to go and check the parent component to see the error.
3.3. Explicitly enforcing the Return Type JSX.Element
Some people like to enforce the return type of the component to be JSX.Element
and they do that by explicitly typing the return type of the component to be JSX.Element
.
3.3.1. Valid return types examples
const FooJSX = (): JSX.Element => {
// Doesn't complain about returning a JSX.Element
return {
type: "div",
key: "foo",
props: {},
};
};
const Foo = (): JSX.Element => {
// Doesn't complain about returning a JSX markup that gets transpiled to JSX.Element under the hood
return <div>Hello World</div>;
};
const Bar = (): JSX.Element => {
return (
<>
<FooJSX /> {/* Doesn't complain about using FooJSX */}
<Foo /> {/* Doesn't complain about using Foo */}
</>
);
};
3.3.2. Invalid return types examples
const FooUndefined = (): JSX.Element => {
// ! ERROR: Type 'undefined' is not assignable to type 'Element'.
return undefined;
};
const FooString = (): JSX.Element => {
// ERROR: Type 'string' is not assignable to type 'Element'.
return "Hello World";
};
const FooNumber = (): JSX.Element => {
// ! ERROR: Type 'number' is not assignable to type 'Element'.
return 123;
};
const FooVoid = (): JSX.Element => {
// ! ERROR: Type 'undefined' is not assignable to type 'Element'.
return;
};
const FooInvalid = (): JSX.Element => {
// ! ERROR: Object literal may only specify known properties, and 'invalid' does not exist in type 'ReactElement<any, any>'.
return {
invalid: "object",
};
};
const Bar = (): JSX.Element => {
return (
<>
<FooUndefined /> {/* Doesn't complain about using FooUndefined */}
<FooString /> {/* Doesn't complain about using FooString */}
<FooNumber /> {/* Doesn't complain about using FooNumber */}
<FooVoid /> {/* Doesn't complain about using FooVoid */}
<FooInvalid /> {/* Doesn't complain about using FooInvalid */}
</>
);
};
3.3.3. Pros and Cons
Pros:
- ✔️ Throws the error inside the faulty component itself so you can see the problem right away and fix it immediately.
- ✔️ Supports generics.
- ✔️ Code is a less verbose.
Cons:
- ❌ Doesn't support returning numbers, strings, and
undefined
. - ❌ Very limiting regarding what is considered a valid return type.
3.4. Explicitly enforcing the Return Type React.ReactNode
I don't see this a lot, but the React.ReactNode
is a type that includes inside of it the JSX.Element
alongside the undefined
, null
, string
, and number
types.
3.4.1. Valid return types examples
const FooUndefined = (): React.ReactNode => {
// Doesn't complain about returning undefined
return undefined;
};
const FooVoid = (): React.ReactNode => {
// Doesn't complain about returning nothing
return;
};
const FooString = (): React.ReactNode => {
// Doesn't complain about using a string
return "Hello World";
};
const FooNumber = (): React.ReactNode => {
// Doesn't complain about using a number.
return 123;
};
const FooJSX = (): React.ReactNode => {
// Doesn't complain about returning a JSX.Element
return {
type: "div",
key: "foo",
children: "Hello, world!",
props: {},
};
};
const Foo = (): React.ReactNode => {
// Doesn't complain about returning a JSX markup that gets transpiled to JSX.Element under the hood
return <div>Hello World</div>;
};
const Bar = (): React.ReactNode => {
return (
<>
<FooUndefined /> {/* Doesn't complain about using FooUndefined */}
<FooVoid /> {/* Doesn't complain about using FooVoid */}
<FooString /> {/* Doesn't complain about using FooString */}
<FooNumber /> {/* Doesn't complain about using FooNumber */}
<FooJSX /> {/* Doesn't complain about using FooJSX */}
<Foo /> {/* Doesn't complain about using Foo */}
</>
);
};
3.4.2. Invalid return types examples
const FooInvalid = (): React.ReactNode => {
// ! ERROR: Object literal may only specify known properties, and 'invalid' does not exist in type 'ReactElement<any, string | JSXElementConstructor<any>> | Iterable<ReactNode> | ReactPortal'.
return {
invalid: "object",
};
};
const Bar = (): React.ReactNode => {
return <FooInvalid />; // Doesn't complain about using FooInvalid
};
3.4.3. Pros and Cons
Pros:
- ✔️ Enforces the return type of the component to be a valid
ReactNode
so you can't return invalid types. - ✔️ Throws the error inside the faulty component itself so you can see the problem right away and fix it immediately.
- ✔️ Supports function declaration and expression while writing a component.
- ✔️ Supports returning numbers, strings, and
undefined
. - ✔️ Supports generics.
- ✔️ Code is a less verbose.
Cons:
- ❌ Doesn't throw an error when the function returns nothing because it's considered as
undefined
, on the other hand theReact.FC
will throw an error in this case.
4. Conclusion
Here's the summary of the approaches described above:
React.FC |
Inferred | JSX.Element |
React.ReactNode |
|
---|---|---|---|---|
Enforces the return type of the component to be a valid React renderable item | ✔️ | ❌ | ✔️ | ✔️ |
Throws the error inside the faulty component itself | ✔️ | ❌ | ✔️ | ✔️ |
Supports function declaration while writing a component | ❌ | ✔️ | ✔️ | ✔️ |
Supports function expression while writing a component | ✔️ | ✔️ | ✔️ | ✔️ |
Supports returning numbers, strings, and undefined
|
✔️ | ✔️ | ❌ | ✔️ |
Supports propTypes , contextTypes , defaultProps , and displayName
|
✔️ | ✔️ | ✔️ | ✔️ |
Throws an error when the function returns nothing | ✔️ | ❌ | ✔️ | ❌ |
Supports generics | ❌ | ✔️ | ✔️ | ✔️ |
Code is less verbose | ❌ | ✔️ | ✔️ | ✔️ |
At the end of the day you will follow the style guide enforced on the project you're working on and you won't have the decision of using one of these different approaches every day, but if you get to choose any of these solutions will work just fine and all of them are good and better than writing in plain JSX, but all I want is to see less hate towards React.FC
as it's actually good right now and it makes total sense to use it in your projects.
If you ask me about my preference it would be as follows.
Explicitly enforce the return type of the components to avoid returning invalid types, and this applies to fresh and well-seasoned developers because you might encounter a complex component with a lot of conditions and you can't see that you're retuning something invalid around there, and having the error thrown inside the component itself will save you a lot of time.
My preference in order would be:
-
React.FC
(I prefer this the most because it's the most true to what's a valid return type and more comprehensive error reporting in the faulty component itself) React.ReactNode
- Inferred return type
-
JSX.Element
(I hate the limitation of not being able to return numbers, strings, andundefined
)
Top comments (1)
Incredible job Karim, very informative, keep it up 👏👏👏