React.FC is better than you think!

Karim Shalapy - Nov 6 - - Dev Community

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

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;
}
Enter fullscreen mode Exit fullscreen mode

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>;
};
Enter fullscreen mode Exit fullscreen mode

You can also use React.FC for components without props:

const Foo: React.FC = () => {
    return <div>Hello World</div>;
};
Enter fullscreen mode Exit fullscreen mode

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 implicit children prop, even if the component didn’t need it. This could lead to unexpected behavior and errors.
  • DefaultProps compatibility: React.FC had issues with defaultProps, making it harder to set default values for component props reliably.
  • Limited return types: It didn’t support returning undefined, string, or number, 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 */}
        </>
    );
};
Enter fullscreen mode Exit fullscreen mode

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 */}
        </>
    );
};
Enter fullscreen mode Exit fullscreen mode

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, and displayName .

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 */}
        </>
    );
};
Enter fullscreen mode Exit fullscreen mode

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. */}
        </>
    );
};
Enter fullscreen mode Exit fullscreen mode

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 */}
        </>
    );
};
Enter fullscreen mode Exit fullscreen mode

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 */}
        </>
    );
};
Enter fullscreen mode Exit fullscreen mode

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 */}
        </>
    );
};
Enter fullscreen mode Exit fullscreen mode

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
};
Enter fullscreen mode Exit fullscreen mode

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 the React.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:

  1. 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)
  2. React.ReactNode
  3. Inferred return type
  4. JSX.Element (I hate the limitation of not being able to return numbers, strings, and undefined)
. .