Essential React Design Patterns for Scalable Apps
This article explores various reactjs design patterns, Compound Component pattern, Render Props, Higher-Order Components (HOCs), Custom Hooks, and Controlled Components. Each approach is discussed in detail, highlighting its benefits, drawbacks, and best use cases, helping you choose the right pattern for your React projects.
Compound Component Pattern
The Compound Component pattern in React is a powerful design technique that allows you to create more flexible and reusable UI components by breaking them down into smaller, composable parts. This pattern is particularly useful for creating components with complex behavior, where different parts need to work together seamlessly.
- Flexibility: Users can compose and arrange child components in any order, giving them more control over the UI structure.
- Reusability: Child components can be reused in different contexts within the parent component or even in other components.
- Encapsulation: The parent component encapsulates the shared state and behavior, keeping the child components simple and focused on their specific tasks.
- Readability: The pattern promotes a clean and declarative API, making the code easier to understand and maintain.
Example: Building a Compound Component
Letās walk through an example of building a Tabs
component using the Compound
Component pattern.
1. Creating the Parent Component (Tabs
)
The Tabs
component will manage the state of which tab is currently active and
provide the necessary context to the child components.
import React, { createContext, useContext, useState } from "react";
const TabsContext = createContext();
const Tabs = ({ children }) => {
const [activeTab, setActiveTab] = useState(0);
const value = {
activeTab,
setActiveTab,
};
return <TabsContext.Provider value={value}>{children}</TabsContext.Provider>;
};
2. Creating Child Components (TabList
, Tab
, TabPanels
, TabPanel
)
Each child component will access the TabsContext
to interact with the state
managed by the Tabs
component.
const TabList = ({ children }) => {
return <div className="tab-list">{children}</div>;
};
const Tab = ({ children, index }) => {
const { activeTab, setActiveTab } = useContext(TabsContext);
return (
<button
className={`tab ${activeTab === index ? "active" : ""}`}
onClick={() => setActiveTab(index)}
>
{children}
</button>
);
};
const TabPanels = ({ children }) => {
return <div className="tab-panels">{children}</div>;
};
const TabPanel = ({ children, index }) => {
const { activeTab } = useContext(TabsContext);
return activeTab === index
? <div className="tab-panel">{children}</div>
: null;
};
3. Using the Compound Component
Now you can compose the Tabs
component with its child components to create the
desired UI.
const App = () => {
return (
<Tabs>
<TabList>
<Tab index={0}>Tab 1</Tab>
<Tab index={1}>Tab 2</Tab>
<Tab index={2}>Tab 3</Tab>
</TabList>
<TabPanels>
<TabPanel index={0}>Content for Tab 1</TabPanel>
<TabPanel index={1}>Content for Tab 2</TabPanel>
<TabPanel index={2}>Content for Tab 3</TabPanel>
</TabPanels>
</Tabs>
);
};
Render Props
Render Props is a pattern where a component takes a function as a prop and uses that function to render content. This pattern provides a way to share logic between components while keeping the UI flexible.
const Tabs = ({ children }) => {
const [activeTab, setActiveTab] = useState(0);
return children({ activeTab, setActiveTab });
};
const App = () => (
<Tabs>
{({ activeTab, setActiveTab }) => (
<>
<div className="tab-list">
<button onClick={() => setActiveTab(0)}>Tab 1</button>
<button onClick={() => setActiveTab(1)}>Tab 2</button>
</div>
<div className="tab-panels">
{activeTab === 0 && <div>Content for Tab 1</div>}
{activeTab === 1 && <div>Content for Tab 2</div>}
</div>
</>
)}
</Tabs>
);
Pros:
- High flexibility: You can control exactly what is rendered and how.
- Logic is easily shared between components.
Cons:
- Can lead to complex code if overused.
- The code may become less readable due to nested functions.
Higher-Order Components (HOCs)
Higher-Order Components are functions that take a component as an argument and return a new component. HOCs are used to add behavior or data to a component, making them a powerful tool for code reuse.
const withTabs = (Component) => {
return (props) => {
const [activeTab, setActiveTab] = useState(0);
return (
<Component {...props} activeTab={activeTab} setActiveTab={setActiveTab} />
);
};
};
const TabsComponent = ({ activeTab, setActiveTab }) => (
<>
<div className="tab-list">
<button onClick={() => setActiveTab(0)}>Tab 1</button>
<button onClick={() => setActiveTab(1)}>Tab 2</button>
</div>
<div className="tab-panels">
{activeTab === 0 && <div>Content for Tab 1</div>}
{activeTab === 1 && <div>Content for Tab 2</div>}
</div>
</>
);
const EnhancedTabs = withTabs(TabsComponent);
const App = () => <EnhancedTabs />;
Pros:
- Can easily add additional behavior to a component.
- Can be reused across different components.
Cons:
- Can lead to āwrapper hellā with deeply nested HOCs.
- Makes the component tree harder to understand and debug.
Custom Hooks
Custom Hooks allow you to extract logic from components into reusable functions. This approach can achieve a similar effect as compound components by sharing state and behavior between components without the need for a hierarchical component structure.
const useTabs = () => {
const [activeTab, setActiveTab] = useState(0);
return { activeTab, setActiveTab };
};
const TabsComponent = () => {
const { activeTab, setActiveTab } = useTabs();
return (
<>
<div className="tab-list">
<button onClick={() => setActiveTab(0)}>Tab 1</button>
<button onClick={() => setActiveTab(1)}>Tab 2</button>
</div>
<div className="tab-panels">
{activeTab === 0 && <div>Content for Tab 1</div>}
{activeTab === 1 && <div>Content for Tab 2</div>}
</div>
</>
);
};
const App = () => <TabsComponent />;
Pros:
- Clean and reusable logic.
- Hooks can be composed and shared across different components.
- Keeps components focused on rendering, while logic is handled by hooks.
Cons:
- May require a different mindset for organizing code, especially for developers new to hooks.
- Less control over the component structure compared to compound components.
Controlled Components
Controlled Components follow a pattern where the parent component fully manages the state and behavior of its children. The children are stateless and rely on the parent for state updates.
const Tabs = ({ activeTab, onTabChange, children }) => {
return React.Children.map(
children,
(child, index) =>
React.cloneElement(child, { activeTab, onTabChange, index }),
);
};
const Tab = ({ index, onTabChange, children }) => (
<button onClick={() => onTabChange(index)}>{children}</button>
);
const TabPanel = ({ index, activeTab, children }) => (
activeTab === index ? <div>{children}</div> : null
);
const App = () => {
const [activeTab, setActiveTab] = useState(0);
return (
<Tabs activeTab={activeTab} onTabChange={setActiveTab}>
<Tab>Tab 1</Tab>
<Tab>Tab 2</Tab>
<TabPanel>Content for Tab 1</TabPanel>
<TabPanel>Content for Tab 2</TabPanel>
</Tabs>
);
};
Pros:
- The parent component has complete control over the state.
- Child components are simple and stateless, making them easier to test and reuse.
Cons:
- Can become cumbersome if many props need to be passed down.
- The parent component can become bloated with too much logic.
Conclusion
Each of these approaches offers different advantages and trade-offs, depending on your use case:
- Compound Components are great for building flexible and composable UI elements.
- Render Props offer maximum flexibility but can lead to complex nesting.
- Higher-Order Components are powerful for adding behavior but can complicate the component tree.
- Custom Hooks provide a clean and reusable way to manage logic, focusing on separation of concerns.
- Controlled Components give full control to the parent but may lead to prop drilling.
Choosing the right pattern depends on your specific needs, the complexity of your UI, and your teamās familiarity with each approach.
Latest blog posts
Explore the world of programming and cybersecurity through our curated collection of blog posts. From cutting-edge coding trends to the latest cyber threats and defense strategies, we've got you covered.