React design patterns are established practices and solutions that developers utilize to address common problems and challenges encountered when building applications with React. These patterns encapsulate reusable solutions to recurring design problems, promoting maintainability, scalability, and efficiency. They provide a structured approach to organizing components, managing state, handling data flow, and optimizing performance.
Consider the following as the top 6 React design patterns:
- Container and presentation pattern
- The HOC(Higher Order Component)Pattern
- Compound Components Pattern
- Provider Pattern(Data management with Providers)
- State reducer pattern
- Component Composition pattern
1. Container and presentation pattern
In this pattern, Container Components are responsible for managing data and state logic. They fetch data from external sources, manipulate it if necessary, and pass it down to Presentational Components as props. They are often connected to external services, Redux stores, or context providers.
On the other hand, Presentational Components are focused solely on the presentation of UI elements. They receive data from Container Components via props and render it in a visually appealing way. Presentational Components are typically stateless functional components or pure components, making them easier to test and reuse.
Let's consider a complex example to illustrate these patterns:
Suppose we're building a social media dashboard where users can view their friends' posts and interact with them. Here's how we could structure our components:
Container Component (FriendFeedContainer):
This component would be responsible for fetching data about friends' posts from an API, handling any necessary data transformations, and managing the state of the feed. It would pass the relevant data down to the Presentational Components.
import React, { useState, useEffect } from 'react';
import FriendFeed from './FriendFeed';
const FriendFeedContainer = () => {
const [friendPosts, setFriendPosts] = useState([]);
useEffect(() => {
// Fetch friend posts from API
const fetchFriendPosts = async () => {
const posts = await fetch('https://api.example.com/friend-posts');
const data = await posts.json();
setFriendPosts(data);
};
fetchFriendPosts();
}, []);
return <FriendFeed posts={friendPosts} />;
};
export default FriendFeedContainer;
Presentational Component (FriendFeed):
This component would receive the friend posts data from its parent Container Component (FriendFeedContainer) as props and render them in a visually appealing way.
import React from 'react';
const FriendFeed = ({ posts }) => {
return (
<div>
<h2>Friend Feed</h2>
<ul>
{posts.map(post => (
<li key={post.id}>
<p>{post.content}</p>
<p>Posted by: {post.author}</p>
</li>
))}
</ul>
</div>
);
};
export default FriendFeed;
By structuring our components this way, we keep the concerns of fetching data and managing the state separate from the UI rendering logic. This separation allows for easier testing, reuse, and maintenance of our React application as it scales.
2.The HOC(Higher Order Component)Pattern
Higher-order components (HOCs) are a pattern in React that allows you to reuse component logic across multiple components. They are functions that take a component and return a new component with additional functionality.
To demonstrate the use of HOCs in a social media dashboard sample with React hooks, let's consider a scenario where you have multiple components that need to fetch user data from an API. Instead of duplicating the fetching logic in each component, you can create an HOC to handle the data fetching and pass the fetched data as props to the wrapped components.
Here's a basic example:
import React, { useState, useEffect } from 'react';
// Define a higher-order component for fetching user data
const withUserData = (WrappedComponent) => {
return (props) => {
const [userData, setUserData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Simulate fetching user data from an API
const fetchData = async () => {
try {
const response = await fetch('https://api.example.com/user');
const data = await response.json();
setUserData(data);
setLoading(false);
} catch (error) {
console.error('Error fetching user data:', error);
setLoading(false);
}
};
fetchData();
}, []);
return (
<div>
{loading ? (
<p>Loading...</p>
) : (
<WrappedComponent {...props} userData={userData} />
)}
</div>
);
};
};
// Create a component to display user data
const UserProfile = ({ userData }) => {
return (
<div>
<h2>User Profile</h2>
{userData && (
<div>
<p>Name: {userData.name}</p>
<p>Email: {userData.email}</p>
{/* Additional user data fields */}
</div>
)}
</div>
);
};
// Wrap the UserProfile component with the withUserData HOC
const UserProfileWithUserData = withUserData(UserProfile);
// Main component where you can render the wrapped component
const SocialMediaDashboard = () => {
return (
<div>
<h1>Social Media Dashboard</h1>
<UserProfileWithUserData />
</div>
);
};
export default SocialMediaDashboard;
In this example:
-
withUserData
is a higher-order component that handles the fetching of user data from an API. It wraps the passed component (WrappedComponent
) and provides the fetched user data as a prop (userData
) to it. -
UserProfile
is a functional component that receives theuserData
prop and displays the user profile information. -
UserProfileWithUserData
is the component returned by wrappingUserProfile
withwithUserData
. -
SocialMediaDashboard
is the main component where you can renderUserProfileWithUserData
or any other component that needs user data.
Using this pattern, you can easily reuse the data fetching logic across multiple components in your social media dashboard application without duplicating code.
3. Compound Components Pattern
The Compound Components pattern in React is a design pattern that allows you to create components that work together to form a cohesive UI, while still maintaining a clear separation of concerns and providing flexibility to customize the components' behavior and appearance.
In this pattern, a parent component acts as a container for one or more child components, known as "compound components." These child components work together to achieve a particular functionality or behavior. The key characteristic of compound components is that they share state and functionality with each other through their parent component.
Here's a simple example of implementing the Compound Components pattern in React using hooks:
import React, { useState } from 'react';
// Parent component that holds the compound components
const Toggle = ({ children }) => {
const [isOn, setIsOn] = useState(false);
// Function to toggle the state
const toggle = () => {
setIsOn((prevIsOn) => !prevIsOn);
};
// Clone the children and pass the toggle function and state to them
const childrenWithProps = React.Children.map(children, (child) => {
if (React.isValidElement(child)) {
return React.cloneElement(child, { isOn, toggle });
}
return child;
});
return <div>{childrenWithProps}</div>;
};
// Child component for the toggle button
const ToggleButton = ({ isOn, toggle }) => {
return (
<button onClick={toggle}>
{isOn ? 'Turn Off' : 'Turn On'}
</button>
);
};
// Child component for the toggle status
const ToggleStatus = ({ isOn }) => {
return <p>The toggle is {isOn ? 'on' : 'off'}.</p>;
};
// Main component where you use the compound components
const App = () => {
return (
<Toggle>
<ToggleStatus />
<ToggleButton />
</Toggle>
);
};
export default App;
In this example:
-
Toggle
is the parent component that holds the compound components (ToggleButton
andToggleStatus
). -
ToggleButton
is a child component responsible for rendering the toggle button. -
ToggleStatus
is another child component responsible for displaying the status of the toggle. - The
Toggle
component manages the state (isOn
) and provides atoggle
function to control the state. It clones its children and passes theisOn
state andtoggle
function as props to them.
By using the Compound Components pattern, you can create reusable and composable components that encapsulate complex UI logic while still allowing for customization and flexibility.
4. Provider Pattern(Data management with Providers)
The Provider Pattern in React is a design pattern used for managing and sharing application state or data across multiple components. It involves creating a provider component that encapsulates the state or data and provides it to its descendant components through React's context API.
Let's walk through an example to illustrate the Provider Pattern in React for managing user authentication data:
// UserContext.js
import React, { createContext, useState } from 'react';
// Create a context for user data
const UserContext = createContext();
// Provider component
export const UserProvider = ({ children }) => {
const [user, setUser] = useState(null);
// Function to login user
const login = (userData) => {
setUser(userData);
};
// Function to logout user
const logout = () => {
setUser(null);
};
return (
<UserContext.Provider value={{ user, login, logout }}>
{children}
</UserContext.Provider>
);
};
export default UserContext;
In this example:
- We create a context called
UserContext
using React'screateContext
function. This context will be used to share user data and authentication-related functions across components. - We define a
UserProvider
component that serves as the provider for theUserContext
. This component manages the user state using theuseState
hook and provides methods likelogin
andlogout
to update the user state. - Inside the
UserProvider
, we wrap thechildren
withUserContext.Provider
and pass theuser
state and thelogin
andlogout
functions as the provider's value. - Now, any component that needs access to user data or authentication-related functions can consume the
UserContext
using theuseContext
hook.
Let's create a component that consumes the user data from the context:
// UserProfile.js
import React, { useContext } from 'react';
import UserContext from './UserContext';
const UserProfile = () => {
const { user, logout } = useContext(UserContext);
return (
<div>
{user ? (
<div>
<h2>Welcome, {user.username}!</h2>
<button onClick={logout}>Logout</button>
</div>
) : (
<div>
<h2>Please log in</h2>
</div>
)}
</div>
);
};
export default UserProfile;
In this component:
We import UserContext
and use the useContext
hook to access the user data and the logout
function provided by the UserProvider
.
Depending on whether the user is logged in or not, we render different UI elements.
Finally, we wrap our application with the UserProvider
to make the user data and authentication-related functions available to all components:
// App.js
import React from 'react';
import { UserProvider } from './UserContext';
import UserProfile from './UserProfile';
const App = () => {
return (
<UserProvider>
<div>
<h1>My App</h1>
<UserProfile />
</div>
</UserProvider>
);
};
export default App;
In this way, the Provider Pattern allows us to manage and share application state or data across multiple components without the need for prop drilling, making our code cleaner and more maintainable.
Top comments (8)
Thanks for the guide! The Compound Composition Pattern seems like something new to me, I will definitely try using it in my projects.
Thanks for sharing the guide with us🌲
Thanks for the guide!
TIL compound components. I never knew Compound was Composition.
Although I used all of these, but I did not know their names or if this is some kind of design pattern. Thank you.
Very useful. Thanks
Is there a VS Code Theme with the same colors? 😁
Loved these articles