By Adam Hultman
As you build more complex React applications, the need for reusable, flexible components becomes more important. Patterns like compound components, render props, and custom hooks allow you to create components that are powerful and adaptable, but they come with their own sets of challenges. In this post, we’ll explore each pattern in depth, discuss when to use them, and show you how to refactor between them as your app evolves.
Before we get into the details, let’s define these three patterns:
useState
, useEffect
, and other hooks to encapsulate complex behaviors.Each pattern has its place, but understanding when to use them is key to building maintainable React applications.
Compound components shine when you have multiple components that need to work together and share context, but you still want to provide flexibility to the user of the component. A classic example is building a form with multiple inputs and controls:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// Accordion.js
import React, { createContext, useContext, useState } from 'react';
const AccordionContext = createContext();
function Accordion({ children }) {
const [openIndex, setOpenIndex] = useState(0);
return (
<AccordionContext.Provider value={{ openIndex, setOpenIndex }}>
<div className="accordion">{children}</div>
</AccordionContext.Provider>
);
}
function AccordionItem({ index, children }) {
const { openIndex, setOpenIndex } = useContext(AccordionContext);
const isOpen = index === openIndex;
return (
<div className="accordion-item">
<div className="accordion-header" onClick={() => setOpenIndex(index)}>
{isOpen ? '▼' : '▶'} {children.header}
</div>
{isOpen && <div className="accordion-body">{children.body}</div>}
</div>
);
}
Accordion.Item = AccordionItem;
export default Accordion;
With this setup, you can use the Accordion
like this:
1
2
3
<Accordion>
<Accordion.Item index={0} header="Item 1" body="This is the content of Item 1" /><Accordion.Item index={1} header="Item 2" body="This is the content of Item 2" />
</Accordion>
Why Use Compound Components?
When to Avoid Compound Components:
React.memo
or context selector patterns.The render props pattern is great for sharing logic between components without relying on context or higher-order components (HOCs). It allows you to define the UI structure in the parent component but still share stateful logic with the child component.
Here’s an example using render props to share hover state:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Hover.js
import React, { useState } from 'react';
function Hover({ children }) {
const [isHovered, setIsHovered] = useState(false);
return children({
isHovered,
onMouseEnter: () => setIsHovered(true),
onMouseLeave: () => setIsHovered(false)
});
}
export default Hover;
Using the Hover
component:
1
2
3
4
5
6
7
<Hover>
{({ isHovered, onMouseEnter, onMouseLeave }) => (
<div onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
{isHovered ? '👀 Hovering!' : '❓ Not hovering'}
</div>
)}
</Hover>
Why Use Render Props?
React.createContext
.When to Avoid Render Props:
Custom hooks are the new kids on the block but have quickly become a favorite among developers. They allow you to extract and reuse logic without having to worry about component structure.
Here’s an example of a custom hook for handling form input state:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// useForm.js
import { useState } from 'react';
export function useForm(initialValues) {
const [values, setValues] = useState(initialValues);
const handleChange = (e) => {
const { name, value } = e.target;
setValues({
...values,
[name]: value,
});
};
return {
values,
handleChange,
};
}
Using the useForm
hook in a component:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { useForm } from './useForm';
function LoginForm() {
const { values, handleChange } = useForm({ username: '', password: '' });
return (
<form>
<input
name="username"
value={values.username}
onChange={handleChange}
placeholder="Username"
/>
<input
name="password"
type="password"
value={values.password}
onChange={handleChange}
placeholder="Password"
/>
</form>
);
}
Why Use Custom Hooks?
When to Avoid Custom Hooks:
Each pattern has unique performance characteristics:
React.memo
or context selectors to optimize.useCallback
to memoize these functions where possible.useEffect
without dependencies.As your app evolves, you may find that a pattern that worked initially is no longer ideal. Here’s how to decide when to refactor:
Choosing the right pattern in React is all about balancing flexibility, simplicity, and maintainability. Compound components provide a way to build highly flexible, context-driven UIs. Render props are great for sharing logic without dictating UI structure. Custom hooks allow you to reuse logic cleanly across components.
Each pattern has its strengths and weaknesses, and the key is understanding when each one is most appropriate. By mastering these patterns, you can build React applications that are not just functional but elegant and maintainable as they grow. Happy coding!