Passing Children

January 6, 2025

4,389 words

Post contents

As we've mentioned before, in the DOM, your HTML elements have a relationship with respect to one another.

For example, the following:

<div>	<ul>		<li>One</li>		<li>Two</li>		<li>Three</li>	</ul></div>

Would construct the following DOM tree:

A top-down DOM tree showing a div at the top, then an ul, then lis beneath the ul, and finally text nodes below the li elements

This is how the DOM constructs nodes as parents and children. Notice how the <li> is distinctly below the <ul> tag rather than a syntax like:

<!-- This isn't correct HTML to do what we want --><div	ul="        li='One'        li='Two'        li='Three'    "/>

While the above looks strange and counter-intuitive, let's look at how we define the same list if each element is a dedicated component using the methods we've created thus far:

const ListItem = ({ name }) => {	return <li>{name}</li>;};const List = () => {	return (		<ul>			<ListItem name="One" />			<ListItem name="Two" />			<ListItem name="Three" />		</ul>	);};const Container = () => {	return (		<div>			<List />		</div>	);};

This is fairly similar to that strange fake nested HTML syntax. The alternative component usage syntax that matches closer to the DOM might otherwise look like this:

<Component>	<OtherComponent /></Component>

This mismatch occurs because if we look at how our components are defined, we're building out our previous components deeply rather than broadly.

A left-to-right chart of a div, then an ul, then back to a vertical top-down chart of li elements with text nodes

This is the difference between building apps with HTML alone and building them with a frontend framework; while the DOM is typically thought of as one-dimensional, there are really two dimensions that are exposed more thoroughly by the framework's ability to construct this tree in a more fine-grained manner.

Let's move the component tree back to being breadth first by using a feature that may sound familiar: Passing children.

Passing Basic Children

Before we explore passing children with our frameworks, let's first think of a potential use case for when we want to do this.

For example, say you want the button to have a "pressed" effect whenever you click on it. Then, when you click on it for a second time, it unclicks. This might look something like the following:

const ToggleButton = ({ text }) => {	const [pressed, setPressed] = useState(false);	return (		<button			onClick={() => setPressed(!pressed)}			style={{				backgroundColor: pressed ? "black" : "white",				color: pressed ? "white" : "black",			}}			type="button"			aria-pressed={pressed}		>			{text}		</button>	);};const ToggleButtonList = () => {	return (		<>			<ToggleButton text="Hello world!" />			<ToggleButton text="Hello other friends!" />		</>	);};

Here, we're passing text as a string property to assign text. But oh no! What if we wanted to add a span inside of the button to add bolded text? After all, if you pass Hello, <span>world</span>!, it wouldn't render the span, but instead render the <span> as text.

A button with the inner text of "Hello, span, world, end span,!"

Instead, let's allow the parent of our ToggleButton to pass in a template that's then rendered into the component.

In React, JSX that's passed as a child to a component can be accessed through a special children component property name:

// "children" is a preserved property name by React. It reflects passed child nodesconst ToggleButton = ({ children }) => {	const [pressed, setPressed] = useState(false);	return (		<button			onClick={() => setPressed(!pressed)}			style={{				backgroundColor: pressed ? "black" : "white",				color: pressed ? "white" : "black",			}}			type="button"			aria-pressed={pressed}		>			{/* We then utilize this special property name as any */}			{/* other JSX variable to display its contents */}			{children}		</button>	);};const ToggleButtonList = () => {	return (		<>			<ToggleButton>				Hello <span style={{ fontWeight: "bold" }}>world</span>!			</ToggleButton>			<ToggleButton>Hello other friends!</ToggleButton>		</>	);};

Here, we're able to pass a span and other elements directly to our ToggleButton component as children.

Using Other Framework Features with Component Children

However, because these templates have the full power of the frameworks at their disposal, these children have superpowers! Let's add a for loop into our children's template to say hello to all of our friends:

function ToggleButtonList() {	const friends = ["Kevin,", "Evelyn,", "and James"];	return (		<>			<ToggleButton>				Hello{" "}				{friends.map((friend) => (					<span>{friend} </span>				))}				!			</ToggleButton>			<ToggleButton>				Hello other friends				<RainbowExclamationMark />			</ToggleButton>		</>	);}function RainbowExclamationMark() {	const rainbowGradient = `    linear-gradient(      180deg,      #fe0000 16.66%,      #fd8c00 16.66%,      33.32%,      #ffe500 33.32%,      49.98%,      #119f0b 49.98%,      66.64%,      #0644b3 66.64%,      83.3%,      #c22edc 83.3%    )  `;	return (		<span			style={{				fontSize: "3rem",				background: rainbowGradient,				backgroundSize: "100%",				WebkitBackgroundClip: "text",				WebkitTextFillColor: "transparent",				MozBackgroundClip: "text",			}}		>			!		</span>	);}

As you can see, we can use any features inside our children - even other components!

Thanks to Sarah Fossheim for the guide on how to add clipped background text like our exclamation mark!

Named Children

While passing one set of elements is useful in its own right, many components require there to be more than one "slot" of data you can pass.

For example, take this dropdown component:

Let's build this dropdown component
These tend to be useful for FAQ pages, hidden content, and more!

This dropdown component has two potential places where passing elements would be beneficial:

<Dropdown>	<DropdownHeader>Let's build this dropdown component</DropdownHeader>	<DropdownBody>		These tend to be useful for FAQ pages, hidden contents, and more!	</DropdownBody></Dropdown>

Let's build this component with an API similar to the above using "named children."

Something worth reminding is that JSX constructs a value, just like a number or string, that you can then store to a variable.

const table = <p>Test</p>;

This can be passed to a function, like console.log, or anything any other JavaScript value can do.

console.log(<p>Test</p>); // ReactElement

Because of this behavior, to pass more than one JSX value to a component, we can use function parameters and pass them that way.

const Dropdown = ({ children, header, expanded, toggle }) => {	return (		<>			<button				onClick={toggle}				aria-expanded={expanded}				aria-controls="dropdown-contents"			>				{expanded ? "V" : ">"} {header}			</button>			<div id="dropdown-contents" role="region" hidden={!expanded}>				{children}			</div>		</>	);};function App() {	const [expanded, setExpanded] = useState(false);	return (		<Dropdown			expanded={expanded}			toggle={() => setExpanded(!expanded)}			header={<>Let's build this dropdown component</>}		>			These tend to be useful for FAQ pages, hidden contents, and more!		</Dropdown>	);}

A simple version of this dropdown component is actually built into the browser as <details> and <summary> HTML tags. Building our own is an experiment intended mostly for learning. For production environment, it's highly suggested to use those built-in elements instead.

Using Passed Children to Build a Table

Now that we're familiar with how to pass a child to a component, let's apply it to one of the components we've been building for our file hosting application: our files "list."

A table of files with headings for "Name", "Last modified", "Type", and "Size"

While this does constitute a list of files, there are actually two dimensions of data: Down and right. This makes this "list" really more of a "table". As such, it's actually a bit of a misconception to use the Unordered List (<ul>) and List Item (<li>) elements for this specific UI element.

Instead, let's build out an HTML table element. A normal HTML table might look something like this:

<table>	<thead>		<tr>			<th>Name</th>		</tr>	</thead>	<tbody>		<tr>			<td>Movies</td>		</tr>	</tbody></table>

Where th acts as a heading data item, and td acts as a bit of data on a given row and column.

Let's refactor our file list to use this DOM layout:

const File = ({ href, fileName, isSelected, onSelected, isFolder }) => {	const [inputDate, setInputDate] = useState(new Date());	// ...	return (		<tr			onClick={onSelected}			aria-selected={isSelected}			style={				isSelected					? { backgroundColor: "blue", color: "white" }					: { backgroundColor: "white", color: "blue" }			}		>			<td>				<a href={href} style={{ color: "inherit" }}>					{fileName}				</a>			</td>			<td>{isFolder ? "Type: Folder" : "Type: File"}</td>			<td>{!isFolder && <FileDate inputDate={inputDate} />}</td>		</tr>	);};const filesArray = [	{		fileName: "File one",		href: "/file/file_one",		isFolder: false,	},	{		fileName: "File two",		href: "/file/file_two",		isFolder: false,	},	{		fileName: "File three",		href: "/file/file_three",		isFolder: false,	},];// This was previously called "FileList"const FileTableBody = () => {	// ...	return (		<tbody>			{filesArray.map((file) => {				return (					<Fragment key={file.id}>						{!file.isFolder && (							<File								fileName={file.fileName}								href={file.href}								isSelected={false}								isFolder={file.isFolder}								onSelected={() => {}}							/>						)}					</Fragment>				);			})}		</tbody>	);};// This is a new componentconst FileTable = () => {	return (		<table style={{ borderSpacing: 0 }}>			<FileTableBody />		</table>	);};

Now that we have an explicit FileTable component, let's see if we're able to style it a bit more with a replacement FileTableContainer component, which uses passing children to style the underlying table element.

const FileTableContainer = ({ children }) => {	return (		<table			style={{				color: "#3366FF",				border: "2px solid #3366FF",				borderSpacing: 0,				padding: "0.5rem",			}}		>			{children}		</table>	);};const FileTable = () => {	return (		<FileTableContainer>			<FileTableBody />		</FileTableContainer>	);};

Challenge

Let's make this chapter's challenge a continuation of the table that we just built in the last section. See, our previous table only had the files themselves, not the header. Let's change that by adding in a second set of children we can pass, like so:

<table>	<FileHeader />	<FileList /></table>
const FileTableContainer = ({ children, header }) => {	return (		<table			style={{				color: "#3366FF",				border: "2px solid #3366FF",				padding: "0.5rem",				borderSpacing: 0,			}}		>			<thead>{header}</thead>			{children}		</table>	);};const FileTable = () => {	const headerEl = (		<tr>			<th>Name</th>			<th>File Type</th>			<th>Date</th>		</tr>	);	return (		<FileTableContainer header={headerEl}>			<FileTableBody />		</FileTableContainer>	);};
Previous articleTransparent Elements
Next article Element Reference

Subscribe to our newsletter!

Subscribe to our newsletter to get updates on new content we create, events we have coming up, and more! We'll make sure not to spam you and provide good insights to the content we have.