Declaratively configuring things in React
Intro
Most commonly, React’s children
prop is used to render/propagate whatever is between your component’s opening and closing tags:
function FancyDiv({ children }) {
return <div>{children}</div>;
}
function App() {
return (
<FancyDiv>
<span>Hello world</span>
</FancyDiv>
);
}
However, the children
prop, when paired with some of React’s utility functions, can do more!
One use is to declaratively express or configure something - all in familiar JSX. This can be a nice alternative to using JSON or React Context.
In this post, we’ll see how we can use React’s children
prop, the props
field available on child elements, and some of React’s utility functions to declartively configure a zoo! 🦁🐯🐧
The Zoo
We’ll use a (contrived!) example of wanting to declare/express the exhibits of a zoo in JSX. To declare these exhibits, we could of course pass the various exhibits as a prop:
<Zoo exhibits={['Lion', 'Tiger', 'Penguin']} />
While this works for simple structures of the exhibits
prop, it will quickly get unwieldy if we wanted to specify more parameters/details of an individual exhibit. Let’s add an openTime
for each exhibit:
// ℹ️ This data would likely come from an API,
// but imagine we just want to hardcode it instead :)
const exhibitInfo = [
{ name: "Lion", openTime: "0800" },
{ name: "Tiger", openTime: "0900" },
{ name: "Penguin", openTime: "0830" },
]
<Zoo exhibits={exhibitInfo} />
Let’s refactor
We can transform the above to be more declarative by introducing a new “exhibit” component which accepts name
and openTime
as props. Instead of JSON configuration passed to the <Zoo>
component as a prop, we’ll now pass the individual <Exhibit>
components to <Zoo>
as children:
<Zoo>
<Exhibit name="Lion" openTime="0800" />
<Exhibit name="Tiger" openTime="0900" />
<Exhibit name="Penguin" openTime="0830" />
</Zoo>
This looks a little cleaner. Additionally, as we’ll see in the next section, we can access the <Exhibit>
props and do something with them from the <Zoo>
component
⬇️
Using the values
After the above refactor to introduce the <Exhibit>
component, we can read the props of each <Exhibit>
and render them out. Depending on your use case, this pattern may be cleaner than using an object, while also utilizing all the features/lifecycle of JSX/React.
To extract the props from the <Exhibit>
components, we’ll use various helper methods of React’s top level API to extract the names of the exhibits:
function Zoo({ children }) {
const exhibitCount = React.Children.count(children);
const exhibitNames = React.Children.map(
children,
(child) => child.props.name
);
// Now that we have the names of the exhibits, we can use them
// however we like! In this example, we'll render them
return (
<div>
<div>There's {exhibitCount} exhibits at the zoo!</div>
<div>The names of the exhibits are: {exhibitNames.join(', ')}</div>
</div>
);
}
function Exhibit({ name, openTime }) {
return null;
}
export function ZooExample() {
return (
<Zoo>
<Exhibit name="Lion" openTime="0800" />
<Exhibit name="Tiger" openTime="0900" />
<Exhibit name="Penguin" openTime="0830" />
</Zoo>
);
}
While we ended up simply rendering the values, you hopefully see how we could build some configuration or pass the values around to other components.
One step further
We can even go deeper than the immediate children
prop of the current component. Continuing with the zoo example above, we’ll add animals to our exhibits with a new <Animal>
component. We’ll then access the names of the animal in our Zoo
component:
function Zoo({ children }) {
const exhibitCount = React.Children.count(children);
const exhibitNames = React.Children.map(
children,
(exhibit) => exhibit.props.name
);
// ℹ️ Note we're accessing the `children` prop of each exhibit!
const animalNames = React.Children.map(children, (exhibit) =>
React.Children.map(exhibit.props.children, (animal) => animal.props.name)
).flat();
return (
<div>
<div>There's {exhibitCount} exhibits at the zoo!</div>
<div>The names of the exhibits are: {exhibitNames.join(", ")}</div>
<div>The names of the animals are: {animalNames.join(", ")}</div>
</div>
);
}
function Exhibit({ name, children }) {
return null;
}
function Animal({ name }) {
return null;
}
export function ZooExample() {
return (
<Zoo>
<Exhibit name="Lion">
<Animal name="Mufasa" />
<Animal name="Nala" />
</Exhibit>
<Exhibit name="Tiger">
<Animal name="Saber" />
</Exhibit>
<Exhibit name="Penguin">
<Animal name="Frodo" />
<Animal name="Sam" />
</Exhibit>
</Zoo>
);
}
Conclusion
You can see how utilizing React’s children
functionality along with some utility methods can be a good way of declaring configuration. Indeed, this is how popular libraries React Navigation and React Router work - which gives users a clean and easy to use declarative API. Take a look at their APIs for some inspiration!