Write React component with reusability in mind
December 14, 2019
In this article, we’ll see a practical example of the render prop pattern. This pattern can be useful to implement when you want to provide a component with some base functionnalities but let the consumer of it controls the presentation, or even extends the behaviour of it, through some state reducer pattern, but that would be another article.
So let’s considere a very simple example of a Dropdown component:
const Dropdown: React.FC = () => {
const [isOpen, toggleOpen] = useState(false)
return (
<div className="dropdown">
<div className="dropdown__toggle" onClick={() => toggleOpen(!isOpen)}>
<button className="dropdown__toggle-btn">Toggle</button>
</div>
{isOpen && (
<div className="dropdown__content">Some content of the dropdown</div>
)}
</div>
)
}
It works well but you cannot reuse it: You have to duplicate the component and change its content and css classes applied to meet the new requirements of your other dropdown. Not that great, let’s change that.
const Dropdown: React.FC = ({ toggleTitle: string, content: JSX.Element }) => {
const [isOpen, toggleOpen] = useState(false)
return (
<div className="dropdown">
<div className="dropdown__toggle" onClick={() => toggleOpen(!isOpen)}>
<button className="dropdown__toggle-btn">{toggle}</button>
</div>
{isOpen && (
<div className="dropdown__content">{content}</div>
)}
</div>
)
}
It is a bit more reusable, but you don’t control the styling applied nor the element used for the toggle. What if you want to toggle on a link or a span instead of a button? This is where the render prop pattern comes handy so the consumer can decide what to display for both the toggle and the content.
So we actually make the Dropdown component juste responsible for the logic of handling the state (isOpen) and updating it. And we completely delegate the presentation concern to the consumer of this component.
And it becomes like this:
type Prop = {
renderToggle: (isOpen, toggleOpen) => JSX.Element
renderContent: (isOpen) => JSX.Element
className?: string;
}
const Dropdown: React.FC<Prop> = props => {
const [isOpen, toggleOpen] = useState(false)
function toggle() {
toggleOpen(!open);
}
return (
<div className={props.className}>
{props.renderToggle(isOpen, toggle)
{isOpen && props.renderContent(isOpen)}
</div>
)
}
Now we give the responsibility to the consumer to actually defines the UI around the toggling logic. So we can create many components around this Dropdown that are just wrapper with different layout and styles.
const PillsDropdown: React.FC = options => {
return (
<Dropdown
className="my-really-special-dropdown"
renderToggle={(isOpen, toggle) => (
<span className="my-really-special-dropdown-toggle" onClick={toggle}>
Toggle me
</span>
)}
renderContent={isOpen => (
<ul className="my-really-special-dropdown-content">
options.map(option => <li>{option.label}</li>)
</ul>
)}
/>
)
}
The next time you find yourself thinking of duplicating a component because you need a slightly different version, or adding an extra prop for handling both cases within the component (and adding a lot of complexity with if
s everywhere), spend some time to actually extract what is the logic from the presentation, and create another consumer with the new layout.