As React developer, we are all familiar with creating components, whether as classes or functions, to add features to web pages.
One of the common cases in React is a component that has JSX Children. When you want to nest content inside a JSX tag, the parent component will receive that content in a prop called children
.
For instance, the Modal component below will receive children
prop set to and render it in a wrapper div.
// React JavaScript
function Modal(props) {
const { children } = props;
return <div className="modal">{children}</div>;
}
function Page() {
return (
<Modal>
<title>Submit Form</title>
<form onSubmit={handleSubmit}>
<input type="text" />
<input type="submit" />
</form>
</Modal>
);
}
If it wants to be converted to TypeScript, most of developers use PropsWithChildren
utility type from @types/react
package.
// React TypeScript
import type { PropsWithChildren } from "react";
// props: {
// children?: React.ReactNode;
// }
function Modal(props: PropsWithChildren<{}>) {
const { children } = props;
return <div className="modal">{children}</div>;
}
function Page() {
return (
<Modal>
<title>Submit Form</title>
<form onSubmit={handleSubmit}>
<input type="text" />
<input type="submit" />
</form>
</Modal>
);
}
It helps TypeScript understand that the component should have the children
prop.
Problems
Sounds good so far, right? But here's where things get weird.
Let me highlight the code.
// props: {
// children?: React.ReactNode; β
// }
// Unexpected β
import type { PropsWithChildren } from "react";
// props: {
// children?: React.ReactNode;
// }
function Modal(props: PropsWithChildren<{}>) {
const { children } = props;
return <div className="card">{children}</div>;
}
function Page() {
return <Modal />;
}
The children
is supposed to be required. That means we must provide children
when using Modal
component. But, guess what? It becomes optional! Rendering Modal
component without children
is now even possible π¨.
Let's check what's inside PropsWithChildren
. Here it is referring to @types/react Github.
type PropsWithChildren<P = unknown> = P & { children?: ReactNode | undefined };
Turns out the culprit is @types/react
itself π΅.
This little bug might not seem like a big deal at first, but trust me, it can cause some serious problems and mess up with your business logic. And the worst part? Neither React nor TypeScript will show any errors, leaving you scratching your head when things go wrong π΅βπ«.
Solutions
So, what can we do about this? Let's explore some solutions!
1. Never Trust and Always Check "third-party" code
As you know earlier, the culprit is @types/react
itself. This is not the first time. Previously, developers had a problem with React.FC
. Some of them even posted about it.
TypeScript + React: Why I don't use React.FC
Why you probably shouldnβt use React.FC to type your React components
Why You Should Probably Think Twice About Using React.FC
All those fancy "magic" codes will eventually lose their "magic" over time. So, you should always check what's behind the code.
2. Create your own Utility type
Since the PropsWithChildren
from @types/react
is not that hard, why don't you create it yourself?
type PropsWithChildren<P = unknown> = P & { children: ReactNode };
PropsWithChildren
utility type is for components that have children
prop and it's required.
type PropsWithOptionalChildren<P = unknown> = P & { children?: ReactNode };
PropsWithOptionalChildren
utility type is for components that have children
prop, but it's optional.
It looks reliable and unambiguous. β
Conclusion
Remember these tips and you'll save yourself from headaches. I hope these tips come in handy for your projects! β¨
Top comments (10)
That's a veey interesting point. It seems it's quite finicky to get the utility prop for react children right.
Would be worth raising this in the react GitHub to get the React team's thoughts on it
Yeah, it would be. Gonna give it a try
IMO itβs not correct that the children should always be required. Prior to react 18 (the latest version), all components were in fact βPropsWithChildrenβ, and the children were optional. In real life (outside of TypeScript) this is still the case- you can always pass/unpass children, only TypeScript will show an error.
The PropsWithChildren is only a mean to mimick the old react functionally more easily. Itβs definitely a use case for children to be optional.
If you want children to be required or be of specific type than just provide the children prop yourself as it serves you.
Yeah, there is no issue regarding the reason behind creating
PropsWithChildren
.Therefore, I just wanted to make it clear to developers who frequently use
PropsWithChildren
about a potential issue and suggested a couple of solutions as I mentioned above.Incorrect comment. How does React handles the children and how your business logic handles the children are 2 completely different things. OP is discussing the latter case, thus totally fine.
Also. Some of the types in the @types/react exists only because removing them would cause breaking change. Nowadays, I tend to just declare the children for each component, be it a number, string, ReactElement, make it specific.
This seems sound, but is unnecessary. The reason is that these two are equivalent:
They're the same because
ReactNode
is a union where one possible value isundefined
. Removing the optional prop?
doesn't change it.You'll see in this TypeScript Playground example both of these types have the same
children
type in a component.It seems equivalent when you focus on the declaration. You will find the big difference when you call both components within the parent components.
I've added some codes on this playground to make it easy to understand. You can take a look.
Why not use
Required<PropsWithChildren>
?Yeah, it can be an option, but it's suitable only for certain cases. Please check the unexpected case as follow:
children
becomes required, but in other casetitle
becomes required too.You can check the code on https://www.typescriptlang.org/play/?ssl=7&ssc=69&pln=7&pc=22#code/JYWwDg9gTgLgBAbzgJQKYEMDGMByEAmqcAvnAGZQQhwDkUG2NAsAFCswCeYRACpWAGcA6sBgALAMJjgAG3z0AdgB4ecALxwArgoDWCiAHcFAPnVxVAMkRxM0uYoD8ALhQNcBIgB8tCwmWAKqPgkANys4Syc3HAA8lDAAOYB6DJ8EIJmaYIi4lKy8qjKSDCiMqjOcAIw8QoJJMbsXERoAI6awPT4WQJmre2dKvzCopJ2BUVwJTBlFVU1dcTGDWwsQA
This isn't an unexpected case. This is how
Required
works. If you do not wanttitle
to be optional, you have to move it out ofRequired
:Wouldn't that be the correct way?