How do we make React components that fit together both now and in the future? A good answer can prevent a lot of rework and heartache. Maybe you’ve already made solid components that meet your project’s needs today. But when those needs change, you might find yourself throwing out good work or spelunking on a giant refactoring mission! Some investigation might yield dividends.
Composition is the fancy word for “how stuff fits together.” Composition is such a big topic in the fields of Software Engineering and Computer Science that it has two extensive Wikipedia entries: Object Composition and Function Composition. As if that were not enough, the entire subfield of Design Patterns and the famous, big book of the same name are all about techniques of composition. However, this article explores specific techniques of composition with React components, not a scientific analysis of the whole field. A little intuition is enough for our purpose.
To build an intuition for composition, think of a blender. The blender itself does just one thing: it spins blades inside a container. What comes out of the blender depends on what you put inside. Fill it with diverse ingredients, and you can make smoothies, soups, salsas, sorbets, and so much more. This year marks 100 years since the blender’s first patent, and it remains a staple for kitchens from small homes to big restaurants. Surely such a successful, simple machine has a lesson or two for us.
So, can we make React components as robust and flexible as blenders? Can we make a component that does its job well and works together with a diverse set of other components? Let’s look at some techniques for how React components can compose.
Children
In React, children are unavoidable–and not very flexible. The children
prop is the default built-in React technique for composing components. To use the prop, simply nest the components in their JSX form.
// Definitions
const ParentComponent =
({ children }) => <div>{children} Smoothie!</div>
const ChildComponent = () => <span>Banana</span>;
// Usage
export default function App() {
return (
<ParentComponent>
<ChildComponent />
</ParentComponent>;
}
The above example code produces this:
Banana Smoothie!
This might seem too simple to satisfy the lofty ideals of composition, but it does! All you need is two components to fit together. We could even replace our child with a new component to meet new requirements. Maintainable code is rarely fancy.
To paraphrase someone important in my upbringing, “Kids. Can’t please ‘em. Can’t fit ‘em in a blender.” Children don’t blend. Our parent component only makes smoothies. If we replaced our “Banana” child component with a “Sourdough” one, it would not make sense. Please do not put bread in a blender, then tell me it’s a smoothie!
Of course, you could copy and paste the parent component and change “Smoothie” to “Breadcrumbs,” but that is the rework we want to avoid. When we use React’s children
prop, we cannot modify the children. We either put it somewhere, or we exclude it entirely. A blender does not simply place tomato, onion, and chiles into a div
; it transforms them into salsa!
On top of non-blendability, the children
prop has another major drawback: you have only one to work with per component. To take a break from the kitchen metaphor, imagine that you want to compose a “meeting” component with both “title” and “date” components. If you want to put those two components in separate places, then the children
prop is not the right choice. The forthcoming techniques have no such restriction.
Benefits of children
Simple to use
Built-in and official
Makes JSX look like normal HTML
Easy to read and comprehend
Limitations of children
Can’t do anything fun with the prop except put it in the right place
Can’t have more than one
Can’t fit ‘em in a blender
Component Instances
A component instance is the value of a JSX fragment. For example, <span>Me</span>
. When you write a functional component and return some JSX, you are returning a component instance.
Because a component instance is a JavaScript value, we can pass one as a prop:
// Definitions
const InstancePropComponent =
({ content }) => <div>{content} Smoothie!</div>;
// Same as Children example
const ChildComponent = () => <span>Peach</span>;
// Usage
export default function App() {
return <InstancePropComponent content={<ChildComponent />} />;
}
The output of this code is similar to the children
example:
Peach Smoothie!
The component’s code looks a lot like the children prop example, but the name of the prop changed. Unlike children
, the content
prop is not built-in; the name “content” is our choice. We are allowed as many such props as we like, and we can put them in separate places.
Because the component instance is evaluated before it is passed as a prop, we have no way to modify it in the parent component. Component instances do not blend. A blender does not simply place butternut squash, garlic, and vegetable broth into separate div
s; it transforms them into soup!
Benefits of Component Instances
Simple to use
Can have many and use them separately
Limitations of Component Instances
Can’t do anything fun with the prop except put it in the right place
It doesn’t blend
Render Props
The React team gave the name “Render Props” to a specific technique for composing components. The most basic use looks like this:
// Definitions
const NullaryRPComponent = ({ fnContent }) => (
<div>{fnContent()} Smoothie!</div>
);
// Usage
export default function App() {
return <NullaryRPComponent
fnContent={() => <span>Strawberry</span>} />;
}
Running that produces the following—which you might expect by now:
Strawberry Smoothie!
Why would we use this? Why not simply use children
or pass a component instance? If this was all you were doing, those are likely better options. But we can go further.
We can add parameters to the function we pass. The above example had no parameters, hence the name “nullary”. Let’s try one parameter:
// Definitions
const UnaryRPComponent = ({ fnContent }) => (
<div>{fnContent(“Frozen”)} Smoothie!</div>
);
// Usage
export default function App() {
return <UnaryRPComponent
fnContent={(state) => <span>{state} Blueberry</span>} />;
}
Which produces:
Frozen Blueberry Smoothie!
With this component, we finally have some superpowers. The UnaryRPComponent
transforms the state of its Render Prop with a parameter. This does not seem so special yet. We could have used a “Frozen Blueberry” component in our previous examples and gotten the same result. How about a more complex example?
// Definitions
const PropsRPComponent = ({ fnContent }) => (
<p>
{
fnContent({
style: { textTransform: ‘uppercase’ },
onClick: () => alert(‘Such Wow! Much Health!’)
})
}
{‘ ‘}
Smoothie!</p>
);
// Usage
export default function App() {
return (
<PropsRPComponent
fnContent={(props) => <span {...props}>Kale</span>}
/>
);
}
Finally, we blend:
KALE Smoothie!
As if by magic, the parent component transformed the content of the Render Prop in two ways. First, the text in the Render Prop is now uppercase. Second, the Render Prop now speaks Doge when clicked.
If the {...props}
syntax blends your brain, do not be alarmed, it is confusing. The result is the same as if we had written the props explicitly, like so: <span style={{...}} onClick={() => {...}>Kale</span>
. This technique is officially named Spread Attributes but is commonly called “Rest Props.” It combines the power of JavaScript’s Spread syntax with JSX.
Unfortunately, Render Props require ugly function calls in the middle of otherwise clean JSX. This feels like chopping something up before putting it in a blender. Surely there is a better way?
I have one more petty gripe with Render Props: Why is this specific technique given this name? When we pass component instances or children
, we’re also rendering a prop. Unfortunately, if we call those “render props,” we may confuse some people.
I know you’re as hungry as I am, but we’ve got one more technique to explore.
Benefits of Render Props
More control than children or component instances
Can run arbitrary code in the passed function
Can have many and use them separately
Limitations of Render Props
Must be called as a function, mismatched with JSX
Vague name for a specific technique
Components
Render Props do not look clean, especially when we want to pass in complex parameters. Passing in a function when we want to provide a component is a bit of a square-peg/round-hole situation. Fortunately, the solution is right in front of us.
All we need to do is capitalize the first letter of the parameter name. Seriously. To use JSX instead of function call syntax is as easy as pressing the “Pulse” button on a blender three times.
// Definitions
const Smoothie = ({ Ingredient }) => (
<p>
<Ingredient
style={{ textTransform: ‘uppercase’ }}
onClick={() => alert(‘Such Wow! Much Health!’)}
/>
{‘ ‘}
Smoothie!</p>
);
const Kale = (props) => <span {...props}>Kale</span>
// Usage
export default function App() {
return <Smoothie Ingredient={Kale} />
}
The result is the same as the previous example:
KALE Smoothie!
I said you only need to do one thing, but this looks very different from the previous example. I admit I cheated. I might as well have added a handful of Oreos to an all-natural smoothie. Let me break down each change:
I changed the parent component’s name to “Smoothie.”
I changed the prop’s name to “Ingredient” (note the capital “I”).
I changed the function call syntax to JSX, with two props instead of one object parameter.
I extracted the passed function unmodified to a component named “Kale.”
After making those changes the result is a far cleaner JSX solution.
Why does this work? A React Function Component is just a function that takes props as an object parameter and returns JSX (a component instance). JSX calls that function behind the scenes, and voila. For more detail on why the prop must be capitalized, see this entry in “JSX in Depth”.
Is this technique still considered “Render Props”? I don’t know. We are no longer explicitly calling a function; JSX does that for us. The only special thing about props is their mapping to JSX.
So, what’s nice about passing components? We get all the transformative power of Render Props with less syntax than passing component instances. On top of that, our parent component can use normal JSX.
The only limitation I can think of at the time of writing is that passing components does not seem to be common practice. That means you may confuse some people with your code. If you can think of other limitations of this technique please let me know!
Benefits of Components
As much control as render props
Can run arbitrary code in the passed component
Can have many, and use them separately
Limitations of Components
Uncommon technique
Conclusion
How do we make React components which fit together well? There is no one right way, but there are better and worse choices. If we are not conscious of our choices, our code may end up tangled and incomprehensible. I hope this exploration has equipped you with some concrete tools for composing components and a better intuition for your future choices.