React: Design Patterns | Container Components
Tags: react, javascript, designpatterns
Published: 2024-06-04 23:13:04

In the world of React, there's a design pattern called Container Components. If you're a beginner or intermediate React developer, you might be used to having each child component load its own data. Typically, you'd use hooks like `useState` and `useEffect` along with libraries like Axios or Fetch to get data from a server. This works, but it can get messy when multiple child components need to share the same logic. That's where container components come in.

Container components handle all the data loading and management for their child components. They abstract away the data-fetching logic, allowing child components to focus solely on rendering. This separation of concerns makes your code cleaner and more maintainable.

## The Main Idea
The main idea behind container components is similar to layout components. Just as layout components ensure that child components don't need to know or care about their layout, container components ensure that child components don't need to know where their data is coming from or how to manage it. They just take some props and display whatever they need to display.

## A Simple Example: CurrentUserLoader
Let's start with a simple example. Suppose we have a `CurrentUserLoader` component that loads the current user's data and passes it to a `UserInfo` component.

```jsx
import React, { useState, useEffect } from 'react';
import axios from 'axios';

export const CurrentUserLoader = ({ children }) => {
  const [user, setUser] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      const response = await axios.get('/current-user');
      setUser(response.data);
    };
    fetchData();
  }, []);

  return (
    <>
      {React.Children.map(children, child => {
        if (React.isValidElement(child)) {
          return React.cloneElement(child, { user });
        }
        return child;
      })}
    </>
  );
};
```

In this example, `CurrentUserLoader` fetches the current user's data and passes it down to its children as a `user` prop. The `UserInfo` component can then use this prop to display the user's information.

## Making It More Flexible: UserLoader
The `CurrentUserLoader` is useful but limited. It only loads the current user's data. What if we want to load any user's data by their ID? We can create a more flexible `UserLoader` component.

```jsx
import React, { useState, useEffect } from 'react';
import axios from 'axios';

export const UserLoader = ({ userId, children }) => {
  const [user, setUser] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      const response = await axios.get(`/users/${userId}`);
      setUser(response.data);
    };
    fetchData();
  }, [userId]);

  return (
    <>
      {React.Children.map(children, child => {
        if (React.isValidElement(child)) {
          return React.cloneElement(child, { user });
        }
        return child;
      })}
    </>
  );
};
```

Now, `UserLoader` can load any user's data by their ID and pass it down to its children.

## Going Generic: ResourceLoader
We can take this concept even further by creating a generic `ResourceLoader` component that can load any type of resource from the server.

```jsx
import React, { useState, useEffect } from 'react';
import axios from 'axios';

export const ResourceLoader = ({ resourceUrl, resourceName, children }) => {
  const [state, setState] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      const response = await axios.get(resourceUrl);
      setState(response.data);
    };
    fetchData();
  }, [resourceUrl]);

  return (
    <>
      {React.Children.map(children, child => {
        if (React.isValidElement(child)) {
          return React.cloneElement(child, { [resourceName]: state });
        }
        return child;
      })}
    </>
  );
};
```

With `ResourceLoader`, you can load any resource by specifying its URL and name. This makes the component highly reusable.

## The Ultimate Flexibility: DataSource
Finally, let's create a `DataSource` component that doesn't even know where its data is coming from. Instead of hardcoding the data-fetching logic, we'll pass a function that returns the data.

```jsx
import React, { useState, useEffect } from 'react';

export const DataSource = ({ getDataFunction, resourceName, children }) => {
  const [state, setState] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      const data = await getDataFunction();
      setState(data);
    };
    fetchData();
  }, [getDataFunction]);

  return (
    <>
      {React.Children.map(children, child => {
        if (React.isValidElement(child)) {
          return React.cloneElement(child, { [resourceName]: state });
        }
        return child;
      })}
    </>
  );
};
```

With `DataSource`, you can load data from any source—whether it's an API, local storage, or something else—by passing a function that returns the data.

## Conclusion
Container components are a powerful pattern in React that help you manage data loading and sharing logic across multiple components. By abstracting away the data-fetching logic into container components like `CurrentUserLoader`, `UserLoader`, `ResourceLoader`, and `DataSource`, you can make your code cleaner and more maintainable. This separation of concerns allows your child components to focus solely on rendering, making your application easier to understand and extend.

---
[React: Design Patterns](https://github.com/LinkedInLearning/react-design-patterns-2895130) is a course by Shaun Wassell that you can follow on LinkedIn Learning.