Three reusable container components — Card, Modal, and PageLayout — that
rely on children and named ReactNode slots instead of encoding every detail
in data props. You also refactored ProjectCard to use Card internally and
moved the inline AddProjectForm into a portal-based Modal.
The configuration approach passes data props to control what a component renders:
<ProjectCard
name="Website Redesign"
status="active"
description="..."
taskCount={5}
/>The composition approach passes JSX as children so the caller controls the structure:
<Card
title="Website Redesign"
footer={<MetaRow taskCount={5} />}
>
<p>Complete overhaul of the company website.</p>
</Card>Neither is universally better. Configuration is simpler when the structure is fixed. Composition wins when callers need to customise inner structure without the container knowing the details.
React.ReactNode is the correct type for the children prop. It covers:
React.ReactElement(JSX expressions)string,number,booleannull,undefined- Arrays of any of the above
Avoid React.FC or React.PropsWithChildren — typing props explicitly is
clearer and more flexible in TypeScript strict mode.
When a component needs multiple distinct areas of content, use additional
React.ReactNode props alongside children:
interface CardProps {
title?: string; // simple string slot — rendered as an h3
footer?: React.ReactNode; // rich slot — anything can go here
children: React.ReactNode;
}This is the "named slots" pattern. Other frameworks call these slots, portals, or transclusion. In React they are just props.
import ReactDOM from 'react-dom'
return ReactDOM.createPortal(
<div className="modal-backdrop" onClick={onClose}>
<div className="modal-dialog" onClick={(e) => e.stopPropagation()}>
{/* content */}
</div>
</div>,
document.body
)The portal renders the JSX into document.body rather than the component's
parent DOM node. This sidesteps any overflow: hidden or z-index stacking
context on ancestor elements — a common cause of modal rendering bugs.
Even though the portal's DOM is attached directly to document.body, React
synthetic events still bubble through the React component tree (not the
DOM tree). This means:
- A click on the dialog fires
onClickon both the dialog and the backdrop (because the dialog is a React child of the backdrop). e.stopPropagation()on the dialog'sonClickstops the event from reaching the backdrop'sonClick.
This is intentional React behaviour and makes portal event handling feel consistent with non-portal components.
children is optional in React 18+. If a consumer forgets to pass children,
the prop is undefined. Rendering undefined in JSX is safe — React renders
nothing. But if you need to check whether children were provided (e.g. to
avoid rendering a wrapper element), use:
import { Children } from 'react'
if (Children.count(props.children) === 0) { ... }A related pattern is render props — passing a function as a prop instead of JSX. The component calls the function to get the content:
<DataList
items={projects}
renderItem={(project) => <ProjectCard key={project.id} {...project} />}
/>This is useful when the child needs data from the parent to render correctly. You will explore this pattern in a later challenge.
Compound components are a more advanced composition pattern where a parent component provides context and child components read from it:
<Tabs>
<Tabs.List>
<Tabs.Tab value="active">Active</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="active">...</Tabs.Panel>
</Tabs>The Tabs.Tab and Tabs.Panel subcomponents are properties of Tabs and
share implicit state via React context. You will build this in a later
challenge.
| Mistake | Fix |
|---|---|
Forgetting e.stopPropagation() on the modal dialog |
Clicking anywhere inside the modal closes it |
Using React.FC with implicit children |
In React 18+, children is not implicit — add it to the interface |
Putting createPortal outside the conditional |
Always guard with if (!isOpen) return null before the portal |
Duplicate card markup in ProjectCard |
Remove the article.project-card wrapper; let Card provide it |
Typing footer as string |
Use React.ReactNode so any JSX can be passed |