In the vast landscape of React, compound components stand out as a powerful pattern, enabling developers to craft more expressive and flexible APIs. But what exactly are compound components, and how can you harness their potential in your applications? Let's dive in.
Understanding Compound Components
A compound component is a type of component that manages the internal state of a feature while delegating control of the rendering to the place of implementation as opposed to the point of declaration.
The idea is that you have two or more components that work together to accomplish a useful task. Typically one component is the parent, and the other is the child. The objective is to provide a more expressive and flexible API.
Another important aspect of this is the concept of "implicit state". The main component stores the state and shares it with its children so that they know how to render themselves based on that state.
A Practical Example
Consider the Toggle
component, which acts as the main component, while On
, Off
, and Button
are its child components. The children are added as static properties to the Toggle
component, making the relationship explicit.
function App() {
return (
<Toggle onToggle={on => console.log(on)}>
<Toggle.On>The button is on</Toggle.On>
<Toggle.Off>The button is off</Toggle.Off>
<Toggle.Button />
</Toggle>
)
}
Behind the Scenes
The Toggle
component manages its state and provides a mechanism to switch between states. It then maps its children to display in the order provided by the user, passing down the necessary state and functions.
class Toggle extends React.Component {
/**
* Define static properties that will be used as children
* Note that each property tracks `on` state
* - On: when the toggle is switched to `on`, we display it's children
* - Off: when the toggle is switched to `off`, we display it's children
* - Button: handles displaying the switch
*/
static On = ({ on, children }) => (on ? children : null)
static Off = ({ on, children }) => (on ? null : children)
static Button = ({ on, toggle, ...props }) => (
<Switch on={on} onClick={toggle} {...props} />
)
state = { on: false }
// Switch the states and then execute the `onToggle`
// function passed down in the callback
toggle = () =>
this.setState(
({ on }) => ({ on: !on }),
() => this.props.onToggle(this.state.on),
)
// Maps children to display in the order provided by the user
render() {
return React.Children.map(this.props.children, childElement =>
React.cloneElement(childElement, {
on: this.state.on,
toggle: this.toggle
})
)
}
}
Embracing Hooks
With the advent of React hooks, we can further refine our compound components. By using contexts and hooks like useState
, useEffect
, and useContext
, we can make our components more concise and readable.
const ToggleContext = createContext();
function Toggle(props) {
const [on, setOn] = useState(false);
const toggle = useCallback(() => setOn(oldOn => !oldOn), []);
const value = useMemo(() => ({ on, toggle }), [on]);
return (
<ToggleContext.Provider value={value}>
{props.children}
</ToggleContext.Provider>
);
}
Conclusion
Compound components offer a robust way to design components that are both flexible and expressive. By understanding the underlying principles and leveraging the power of hooks, developers can create more maintainable and scalable React applications.
For a hands-on experience, check out this CodeSandbox example. For a deeper dive into compound components and advanced React patterns, these resources are invaluable: