React Component Design Patterns - Part 1

Fatemeh Paghar - Mar 26 - - Dev Community

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:

  1. Container and presentation pattern
  2. The HOC(Higher Order Component)Pattern
  3. Compound Components Pattern
  4. Provider Pattern(Data management with Providers)
  5. State reducer pattern
  6. 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;

Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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 the userData prop and displays the user profile information.
  • UserProfileWithUserData is the component returned by wrapping UserProfile with withUserData.
  • SocialMediaDashboard is the main component where you can render UserProfileWithUserData 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;

Enter fullscreen mode Exit fullscreen mode

In this example:

  • Toggle is the parent component that holds the compound components (ToggleButton and ToggleStatus).
  • 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 a toggle function to control the state. It clones its children and passes the isOn state and toggle 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;

Enter fullscreen mode Exit fullscreen mode

In this example:

  • We create a context called UserContext using React's createContext 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 the UserContext. This component manages the user state using the useState hook and provides methods like login and logout to update the user state.
  • Inside the UserProvider, we wrap the children with UserContext.Provider and pass the user state and the login and logout functions as the provider's value.
  • Now, any component that needs access to user data or authentication-related functions can consume the UserContext using the useContext 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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .