Frontend
Intermediate
Josué Hernández

Josué Hernández

    What is the Container-Presenter Pattern?
    🧠 Container Components:
    🎨 Presenter Components:
    Benefits of the Container-Presenter Pattern
    Potential Drawbacks
    When to Use the Container-Presenter Pattern
    Real-World Use Cases
    Implementing the Container-Presenter Pattern: A Practical Example
    1. Container Component (DataContainer.tsx)
    2. Presenter Component (DataPresenter.tsx)
    Best Practices for the Container-Presenter Pattern
    Conclusion
    Additional Resources

The Container-Presenter pattern is a design pattern in React that promotes a clean and maintainable architecture by separating the business logic from the UI presentation. This approach ensures that components remain reusable, testable, and easier to maintain, especially as your application scales.


What is the Container-Presenter Pattern?

The Container-Presenter pattern involves splitting components into two distinct types:

🧠 Container Components:

  • Manage state, side effects, and data fetching.
  • Contain all business logic.
  • Pass data and actions to Presenter components as props.
  • Handle API calls, state management, and error handling.

🎨 Presenter Components:

  • Focus solely on UI rendering.
  • Receive data and actions as props.
  • Are stateless, avoiding side effects.
  • Primarily concerned with how data is displayed, not how it is fetched or managed.

Benefits of the Container-Presenter Pattern

  • Separation of Concerns: Keeps the business logic and UI logic independent, leading to better-organized codebases.
  • Reusability: Presenter components can be easily reused across different containers or projects.
  • Simplified Testing: Testing Presenter components is straightforward since they are pure functions with no dependencies on external state.
  • Improved Maintainability: When business logic or API endpoints change, you only need to update the Container component without touching the UI components.
  • Scalability: Ideal for large applications where clear component responsibilities prevent complexity from spiraling out of control.

Potential Drawbacks

  • Boilerplate Code: May introduce additional components, resulting in more files and a bit of setup overhead.
  • Prop Drilling: When not combined with state management tools like Redux or React Context, passing props down multiple layers can become cumbersome.
  • Overhead for Small Apps: In smaller applications, this pattern might be over-engineering, adding unnecessary complexity.

When to Use the Container-Presenter Pattern

  • When building large-scale applications that require modular and maintainable code.
  • For scenarios with complex data fetching, state management, or business logic.
  • When you need to reuse UI components with different data sources or behaviors.
  • To improve code maintainability and simplify testing.

Real-World Use Cases

  1. Data-Driven Dashboards: Separate data fetching and transformation from the visualization components.
  2. E-commerce Applications: Isolate business logic related to cart management, product fetching, and checkout flows.
  3. Form Management Systems: Centralize form data handling, validation, and submission logic in Container components.

Implementing the Container-Presenter Pattern: A Practical Example

Below is a practical example of implementing the Container-Presenter pattern to handle data fetching, loading states, and error management while keeping the UI rendering isolated.

1. Container Component (DataContainer.tsx)

TYPESCRIPT
import { useEffect, useState } from "react";
import { DataPresenter } from "./DataPresenter";

type DataItem = {
  id: number;
  name: string;
  description: string;
  image: string;
};

export const DataContainer = () => {
  const [data, setData] = useState<DataItem[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch("/data/data.json");
        if (!response.ok) {
          throw new Error("Failed to load data");
        }
        const result = await response.json();
        setData(result);
      } catch (error) {
        setError(`Error loading data: ${(error as Error).message}`);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, []);

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

  return <DataPresenter data={data} />;
};

2. Presenter Component (DataPresenter.tsx)

TYPESCRIPT
import React from "react";

type DataItem = {
  id: number;
  name: string;
  description: string;
  image: string;
};

type DataPresenterProps = {
  data: DataItem[];
};

export const DataPresenter: React.FC<DataPresenterProps> = ({ data }) => {
  if (!data.length) {
    return <p>No data available.</p>;
  }

  return (
    <div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: "10px" }}>
      {data.map((item) => (
        <div key={item.id} style={{ border: "1px solid #ddd", padding: "10px" }}>
          <img src={item.image} alt={item.name} style={{ width: "100%", height: "auto" }} />
          <h3>{item.name}</h3>
          <p>{item.description}</p>
        </div>
      ))}
    </div>
  );
};

Best Practices for the Container-Presenter Pattern

  1. Use TypeScript: Strongly type props to avoid runtime errors.
  2. Avoid Prop Drilling: For large applications, combine this pattern with state management solutions like Redux or React Context.
  3. Simplify Containers: If the Container component becomes too complex, consider extracting logic into Custom Hooks.
  4. Keep Presenters Pure: The Presenter component should not include state or side effects.

Conclusion

The Container-Presenter pattern is a proven technique for building scalable, maintainable, and reusable components in React applications. By separating the business logic from the UI presentation, developers can create flexible components that are easier to test, maintain, and reuse.

This pattern is particularly useful for data-heavy applications, large-scale projects, or any scenario where maintainability is a priority.

In future posts, we'll explore how to combine this pattern with state management tools and advanced component design patterns to create even more powerful and dynamic applications.


Additional Resources


Josué Hernández
Josué Hernández

Last Update on 2025-03-08

Related Blogs

© 2024 Effort Stack. All rights reserved