A dumb component renders any text passed while its visual appearance remains static. It is also unknown how many new content variations are about to come. What would be a useful React pattern in this scenario?
The Problem
Look at the right side of the picture below.
It displays two lines of text and a link. You will face no issues representing it with a single component until the content becomes dynamic and texts/link need to change. In such case presentation (aka design) is the same, but content may have fluid variations.
The challenge is to organize your code, so it remains simple and allows for painless future changes.
There are three things to take care of along the way: styling, data, and business logic.
The Solution
It aims for flexibility and maintainability. Making future changes should be straightforward as that is the real pain point here — prepare the code for hidden information causing new uses cases to pop in.
Folder Structure
-- components
|-- Box
|-- Box.js
|-- Box.css
|-- components
|-- Text1.js
|-- Text2.js
|-- Text3.js
|-- Text4.js
The solution involves two-component levels — parent component (<Box />
) and several child components for each textual case (<TextN />
). Note that child component names above are chosen to simplify the picture. Ideally, you should name them after each specific use case, fx. StorageEmptyText.js
, StorageAboveAverageText.js
, StorageFullText.js
, etc.
Box Component
That's the parent (container) component. Its purpose is to manage business logic and styling.
// Box.js
import StorageEmptyText from "./components/StorageEmptyText";
import StorageAboveAverageText from "./components/StorageAboveAverageText";
import StorageFullText from "./components/StorageFullText";
export function Box({
storage,
openDialogAction,
manageStorageAction,
upgradeToProPlanAction
}) {
let TextComponent = () => null;
let handleClick = () => null;
// 1️⃣ Use case logic: conditionally assign text component and click action
if (storage === 0) {
TextComponent = StorageEmptyText;
handleClick = openDialogAction;
} else if (storage > 50 && storage < 80) {
TextComponent = StorageAboveAverageText;
handleClick = manageStorageAction;
} else if (storage >= 90) {
TextComponent = StorageFullText;
handleClick = upgradeToProPlanAction;
}
// 2️⃣ More cases to follow
return (
<div className="Box">
{/* 3️⃣ A component with render prop is famous for its flexibility */}
<TextComponent>
{({ title, description, link }) => (
<>
{/* 4️⃣ Box.js (the parent) holds these elements' styling */}
<h3>{title}</h3>
<p>{description}</p>
<span onClick={handleClick}>{link}</span>
</>
)}
</TextComponent>
</div>
);
}
Several benefits emerge from such a structure:
- The markup around each text is part of
Box.js
so you can style it here. - The use case logic is explicit.
- Child components are nearby.
Having texts encapsulated in <TextComponent />
s instead of a function or hooks will also lighten the tests. You will need to check the className
or data-test-id
presence for each use case, not the actual texts.
// Box.test.js
expect(
<Box storage={95} />,
"when mounted",
"to contain elements matching",
".StorageFullText"
);
Text Components
You may have any number of these components. They share the same structure — dumb component with a render prop function to accept the texts. File names should be descriptive as that gives you a hint of what's going on by both, looking at the folder structure and maintaining the logic in Box.js
// StorageFullText.js
export default function StorageFullText({ children }) {
return (
<div className="StorageFullText">
{/*
Passes down its texts to a children() function
That allows for custom styling in the parent
*/}
{children({
title: "Looks like you have too many items",
description: "Upgrade to our Pro plan or free up more space in your box.",
link: "See all plans"
})}
</div>
);
}
<StorageFullText />
uses render prop to send back data and is not aware of how texts are consumed later on. One of the reasons for using this pattern is the flexibility render prop components provide over hooks fx.
Final Words
Handling text components may look simple on the surface, but in some situations requires you to take a different approach and the extra step.
If a component doesn't change its visual representation and structure but may have many content variants, it makes sense to involve fewer wrappers if possible. The guiding principle is to avoid being smart about code reusability. Instead, go explicit and minimal expecting things to change.
📩
I maintain a monthly blog newsletter with more posts like this.
Top comments (2)
Really interesting, thanks for sharing ! I'm really curious to test out this pattern and see how it goes when the "variant" components (the Text components in your example) tend to vary in terms of props, including some additional ones that may need to come from above ! Might be a little tricky
That will bring some load to
Box.js
as it will need to know about these props.If the overall output of the text components remains the same — meaning that at the end they still return text with the same structure — but the props change, you can do something like that:
Or render (not assign)
<TextComponent />
as part of the Box's conditional logic. It is a matter of taste.Some comments have been hidden by the post's author - find out more