Editor’s note: This article was last updated by Emmanuel Odioko on 13 September 2024 to include the type safety benefits of using wrapper components in TypeScript, a discussion on using React.FC to pass components as props, and instructions on how to clone and update child components using React.cloneElement, which you can read more about here.
Typing the children
prop in React can be challenging at first. If you try typing them as specific JSX types, you may run into issues rendering the child components. There is also the problem with the paradox of choice, as there are multiple available options to type the children
prop, which may lead to decision fatigue.
In this article, I’ll provide recommended solutions to effectively type the children
prop, addressing common issues and helping you avoid decision fatigue.
The basic usage of the children
prop is to receive and manipulate the content passed within the opening and closing tags of a JSX expression. When you write a JSX expression with opening and closing tags, the content passed between them is referred to as their child:
Consider the following example:
<Border> Hey, I represent the JSX children! </Border>
In this example, the literal string Hey, I represent the JSX children!
refers to the child rendered within Border
.
Meanwhile, to gain access to the content passed between JSX closing and opening tags, React passes these in a special prop: props.children
. For example, Border
could receive the children
prop as follows:
const Border = ({children}) => { return <div style={{border: "1px solid red"}}> {children} </div> }
Border
accepts the children
prop, then renders children
within a div
with a border style of 1px solid red
.
Strictly speaking, there are a handful of supported content types that can go within the opening and closing tags of your JSX expression. Below are some of the most commonly used ones.
Literal strings are valid children types. The example below shows how to pass one into a component:
<YourComponent> This is a valid child string </YourComponent />
Note that in YourComponent
, props.children
will be the string This is a valid child string
.
You can also pass other JSX elements as valid children types. This is usually helpful when composing different nested components. Below is an example:
<Wrapper> <YourFirstComponent /> <YourSecondComponent /> </Wrapper>
It is also completely acceptable to mix children types, as seen here:
<Wrapper> I am a valid string child <YourFirstComponent /> <YourSecondComponent /> </Wrapper>
Expressions are equally valid children types. As shown below, myScopedVariableReference
can be any JavaScript expression:
<YourFirstComponent> {myScopedVariableReference} </YourFirstComponent>
Remember that expressions in JSX
are written in curly braces.
Functions are equally valid children types, as shown below:
<YourFirstComponent> {() => <div>{myScopedVariableReference}</div>} </YourFirstComponent>
As you can see, the children
prop can be represented by a large range of data types. Your first inclination might be to type these out manually, like so:
type Props = { children: string | JSX.Element | JSX.Element[] | () => JSX.Element } const YourComponent = ({children} : Props) => { return children }
But this doesn’t fully represent the children
prop. What about fragments, portals, and ignored render values, such as undefined
, null
, true
, or false
?
A full representation may look something like this:
type ReactText = string | number; type ReactChild = ReactElement | ReactText; interface ReactNodeArray extends Array<ReactNode> {} type ReactFragment = {} | ReactNodeArray; type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean | null | undefined; type Props = { children: ReactNode } // source: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/d076add9f29db350a19bd94c37b197729cc02f87/types/react/index.d.ts
See the extended types for ReactPortal
and ReactElement
. Do they look complex? There’s a good chance they do.
The point I’m trying to make is that, in practice, you don’t want to manually type the children
prop. Instead, I suggest using the officially supported types discussed below.
PropsWithChildren
typeThe React.PropsWithChildren
type takes your component prop and returns a union type with the children
prop appropriately typed. No extra work from you is needed.
In practice, here’s the definition of the PropsWithChildren
type:
type PropsWithChildren<P> = P & { children?: ReactNode };
Assume you had a component Foo
with FooProps
:
type FooProps = { name: 'foo' } export const Foo = (props: FooProps) => { return null }
You can introduce the children
prop as follows:
import { PropsWithChildren } from 'react' type FooProps = { name: 'foo' } export const Foo = (props: PropsWithChildren<FooProps>) => { return props.children }
When you pass PropsWithChildren
to FooProps
, you get the children
prop internally typed.
In most cases, this is the recommended way to type the children
prop because it requires less boilerplate and the children
prop is implicitly typed.
ReactNode
typeIn cases where you must explicitly type the children
prop, you can use the ReactNode
type. Remember the definition for the PropsWithChildren
type:
type PropsWithChildren<P> = P & { children?: ReactNode };
Instead of relying on PropsWithChildren
, you can also type the children
prop directly:
import { ReactNode } from 'react' type FooProps = { name: 'foo' // look here 👇 children: ReactNode } export const Foo = (props: FooProps) => { return props.children }
FunctionComponent
(or FC
) typeThe FunctionComponent
generic interface may also be used to appropriately type the children
prop. Internally, this interface relies on PropsWithChildren
.
Here’s how you’d use it:
import { FunctionComponent, ReactElement } from 'react' interface FooProps extends React.PropsWithChildren { name: 'foo', children: ReactElement } export const Foo: FunctionComponent<FooProps> = (props) => { return props.children }
Note that the FC
type is an alias for FunctionComponent
. Their usages are similar, as shown below:
import { FC, ReactElement } from 'react' interface FooProps extends React.PropsWithChildren { name: 'foo', children: ReactElement } export const Foo: FC<FooProps> = (props) => { return props.children }
Component
type for class componentsMost modern React codebases no longer use class components, except in specific use cases. If you find yourself needing to type the children
prop in a class component, leverage the Component
prop, as shown below:
import { Component } from 'react' type FooProps = { name: 'foo' } class Foo extends Component<FooProps> { render() { return this.props.children } }
The Component
type automatically includes the children
prop.
ReactElement
You can also use the ReactElement
interface to appropriately type the children
prop. ReactElement
is a subset of ReactNode
:
import { ReactElement } from 'react' type Props = { name: 'foo' } export const Foo = (props: { children: ReactElement<Props>}) => { return props.children }
JSX.Element
JSX.Element
can also be an appropriate type for the children
prop. Below is an example showing how to implement it:
import { JSX } from "react"; export const Foot = (props: { children: JSX.Element }) => { return props.children; };
As a bonus to this section, you must remember that wrapper components are simple higher-order components used to wrap other components for easy interaction. When dealing with TypeScript, wrapper components can improve type safety especially when handling complex children structures.
Let’s demonstrate the type safety benefits of wrapper components using a to-do application. In the demo application, I provided a few ways that wrapper components can help provide type safety benefits. Each component in the application defines unique type interfaces for its props. Here’s an example:
interface AddTodoProps { addTodo: (text: string) => void; }
AddTodo
expects a function for the addTodo
prop that takes a string
as an argument and returns nothing. TypeScript ensures that whenever AddTodo
is used, the addTodo
prop follows the correct type.
And if you try to pass an incorrect prop type, TypeScript will throw a compile-time error:
// Incorrect prop type; <AddTodo addTodo={(num: number) => {}} />
You will get the error below, as well as a possible fix:
This helps us to make fewer mistakes while coding.
Lastly, for complex data structures like our todos.tsx
, it contains multiple properties:
export interface Todo { id: number; text: string; completed: boolean; }
When passing a todo
between components, TypeScript ensures that all required properties are present and, as earlier mentioned, correctly typed. This takes us to our next section on passing components as props in TypeScript.
React.FC
Passing components as props in React often means using the React.FC
type, which helps create reusable, strongly typed components that can accept other components as their props.
Let’s make quick edits to our project. We will be passing components as props using the React.FC
type and TypeScript generic capabilities. In the project, we will create a button component instead of hardcoding it within the AddTodo
component. Our AddTod.tsx
will look like this:
interface AddTodoProps { addTodo: (text: string) => void; ButtonComponent: React.FC<{ onClick: () => void }>; // Here we wiil be expecting a Button as a prop } const AddTodo: React.FC<AddTodoProps> = ({ addTodo, ButtonComponent }) => { const [text, setText] = useState(''); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (!text.trim()) return; addTodo(text); setText(''); }; return ( <form onSubmit={handleSubmit} className="flex mb-4"> <input type="text" value={text} onChange={e => setText(e.target.value)} placeholder="Add a new todo" className="flex-grow border rounded-l text-black px-3 py-2 focus:outline-none" /> <ButtonComponent onClick={handleSubmit} /> <-- This is the Button component, that will be passed as a prop. </form> ); }; export default AddTodo;
Next, let’s create our custom Button
component:
CustomButton.tsx import React from 'react'; interface CustomButtonProps { onClick: () => void; } const CustomButton: React.FC<CustomButtonProps> = ({ onClick }) => { return ( <button type="button" onClick={onClick} className="bg-green-500 text-white px-4 py-2 rounded-r hover:bg-green-600" > Custom Add </button> ); }; export default CustomButton;
We will need to make edits to the TodoApp.tsx
component so we can pass our CustomButton.tsx
to AddTodo.tsx
as a prop:
TodoApp.tsx import CustomButton from './CustomButton'; {/* To save space, this is the only line you should make edits to after imports*/} <AddTodo addTodo={addTodo} ButtonComponent={CustomButton} />
Using React.FC
allows us to easily handle generic props with children because it automatically includes the children prop. This is useful when a component needs to render its children without explicitly defining children in the component’s prop types.
When using React.FC
, you can define prop types using an interface, and TypeScript will prevent bugs by checking that the components are correctly used in other parts of the project.
What happens if you do not use React.FC
? Would you still be able to pass components and type-check props? The straight answer is, yes you will. But if you don’t use React.FC
, you can still pass components and type-check props, but you’ll need to manually declare the children prop. Without React.FC
, TypeScript won’t assume the component accepts children, losing that built-in convenience.
React.cloneElement
Warning from React: Using `cloneElement` is uncommon and can lead to fragile code. [See common alternatives.](https://react.dev/reference/react/cloneElement#alternatives)
TypeScript offers very minimal help managing issues with React.cloneElement
. It offers similar benefits as the alternative approaches, but cannot fully resolve the challenges because of its React’s architectural complexity.
Here’s how you can clone and update child components using React.cloneElement
in our project. In this example, we clone a TodoItem
component and add a highlight prop that changes the background color to yellow when clicked.
Replace the TodoItem.tsx
with the code below:
import React from 'react'; import { Todo } from './types/todos'; interface TodoItemProps { todo: Todo; toggleTodo: (id: number) => void; highlight?: boolean; } const TodoItem: React.FC<TodoItemProps> = ({ todo, toggleTodo, highlight }) => { return ( <li onClick={() => toggleTodo(todo.id)} className={`p-2 border rounded cursor-pointer select-none ${ todo.completed ? 'line-through text-black' : '' } ${highlight ? 'bg-yellow-300' : ''}`} // Here is the highlight effect > {todo.text} </li> ); }; export default TodoItem;
Now, we will update the TodoList
component to use React.cloneElement
to clone each TodoItem
and add the highlight
prop whenever the todo is completed:
TodoList.tsx //I only highlighted the code that needed to be modified!! return ( <ul className="space-y-2 text-black"> {todos.map(todo => { return React.cloneElement( <TodoItem todo={todo} toggleTodo={toggleTodo} />, { highlight: todo.completed } ); })} </ul> ); };
This is a simple way of using React.cloneElement
in your project to safely update children with additional props. Check out our guide to using React.cloneElement
to clone elements for more information.
In this article, we learned the best practices for typing children
props in TypeScript in both functional and class-based components. Where possible, it is recommended to use the PropsWithChildren
type as opposed to manually typing the children
props or using any other approach listed.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to implement one-way and two-way data binding in Vue.js, using v-model and advanced techniques like defineModel for better apps.
Compare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
2 Replies to "Using the React children prop with TypeScript"
Thankyou for this resource
FYI, since React 18, the FC type no longer contains the children prop.