React is a popular JavaScript library for building user interfaces. As applications grow in complexity, developers adopt design patterns to manage state and UI logic effectively. Two such patterns are the Container/Presenter pattern and Compound Components. Understanding these patterns helps create more maintainable and reusable React components.

Container/Presenter Pattern

The Container/Presenter pattern separates concerns within components. The container component manages state and logic, while the presenter component focuses solely on rendering UI based on props. This separation makes components easier to test, reuse, and maintain.

How It Works

The container component handles data fetching, state management, and event handling. It passes necessary data and callbacks as props to the presentational component, which renders the UI. This division allows developers to modify presentation or logic independently.

Example

Consider a user profile feature. The container fetches user data and manages loading states, while the presenter displays the user information.

function UserContainer() {
  const [user, setUser] = React.useState(null);
  const [loading, setLoading] = React.useState(true);

  React.useEffect(() => {
    fetchUserData().then(data => {
      setUser(data);
      setLoading(false);
    });
  }, []);

  if (loading) {
    return <p>Loading...</p>;
  }

  return <UserPresenter user={user} />;
}

function UserPresenter({ user }) {
  return (
    <div>
      <h2>{user.name}</h2>
      <p>Email: {user.email}</p>
    </div>
  );
}

Compound Components

Compound Components allow related components to work together seamlessly by sharing implicit state or context. They enable developers to create flexible, declarative APIs that are easy to compose and extend.

How It Works

Instead of passing props explicitly, compound components use React's Context API to share state. The parent component provides context, while child components consume it, allowing for more natural composition.

Example

Imagine a custom Tabs component where TabList and TabPanel are separate components but work together through context.

const TabsContext = React.createContext();

function Tabs({ children, defaultIndex = 0 }) {
  const [selectedIndex, setSelectedIndex] = React.useState(defaultIndex);

  return (
    <TabsContext.Provider value={{ selectedIndex, setSelectedIndex }}>
      <div className="tabs">{children}</div>
    </TabsContext.Provider>
  );
}

function TabList({ children }) {
  return <div className="tab-list">{children}</div>;
}

function Tab({ index, children }) {
  const { selectedIndex, setSelectedIndex } = React.useContext(TabsContext);
  const isSelected = index === selectedIndex;

  return (
    <button
      className={isSelected ? 'active' : ''}
      onClick={() => setSelectedIndex(index)}
    >
      {children}
    </button>
  );
}

function TabPanel({ index, children }) {
  const { selectedIndex } = React.useContext(TabsContext);
  return selectedIndex === index ? <div className="tab-panel">{children}</div> : null;
}

// Usage
<Tabs>
  <TabList>
    <Tab index={0}>Tab 1</Tab>
    <Tab index={1}>Tab 2</Tab>
  </TabList>
  <TabPanel index={0}>Content for Tab 1</TabPanel>
  <TabPanel index={1}>Content for Tab 2</TabPanel>
</Tabs>

Choosing the Right Pattern

Both patterns improve component organization but serve different purposes. Use the Container/Presenter pattern when you want clear separation of concerns, especially for complex data handling. Choose Compound Components when building flexible, composable UI elements that share state or context.

Summary

Understanding React patterns like Container/Presenter and Compound Components enhances your ability to build scalable, maintainable applications. They promote code reuse, improve readability, and facilitate testing, making your development process more efficient.