React Component Design Patterns - Part 1
Tags: react, javascript, frontend
Published: 2024-03-26 19:29:29

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.

```Javascript
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.

```Javascript
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:

```Javascript
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 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:

```Javascript
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` 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:

```Javascript
// 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'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:

```Javascript
// 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:

```Javascript
// 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.