Design patterns in React - Container and Presentational Components Pattern in React
Photo by Toa Heftiba on Unsplash
The Container and Presentational components pattern in React is a powerful approach for organizing React applications. This pattern not only helps in maintaining a clean codebase but also in streamlining the development process.
Containers:
Characteristics:
State Management: Container components are responsible for managing and manipulating the state. They handle complex logic which may involve interactions with Redux stores, local state, or Context API.
Data Handling: Containers often care for data fetching, processing data, and error handling. They might interact with APIs or services, parse data, and prepare it in a format that the presentational components can easily display.
Business Logic: They encapsulate the business logic of the app, such as making decisions, computing values, or managing user interactions, making it independent of the UI.
Example Scenario:
Let's create a simple example showcasing the Container and Presentational components pattern with a ProductListContainer
and a ProductList
component in a hypothetical e-commerce application.
ProductListContainer (Container Component)
The ProductListContainer
is responsible for fetching product data and managing state. It encapsulates the logic and passes the necessary data to the presentational component.
Assuming we are using an Apollo client to fetch the data.
// apolloClient.ts
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';
const httpLink = new HttpLink({
uri: 'your-graphql-endpoint',
});
const client = new ApolloClient({
link: httpLink,
cache: new InMemoryCache(),
});
export default client;
Using the Apollo Provider
To make Apollo Client available in your React component tree, you need to wrap your application with ApolloProvider
in the root component (usually in App.tsx
).
// App.tsx
import React from 'react';
import { ApolloProvider } from '@apollo/client';
import apolloClient from './apolloClient';
import ProductListContainer from './ProductListContainer';
function App() {
return (
<ApolloProvider client={apolloClient}>
<ProductListContainer />
</ApolloProvider>
);
}
export default App;
The ProductListContainer
to use the useQuery
hook from Apollo Client.
// ProductListContainer.tsx
import React from 'react';
import { useQuery, gql } from '@apollo/client';
import ProductList from './ProductList';
// Define a type for the product
type Product = {
id: number;
name: string;
description: string;
price: number;
};
// GraphQL query
const GET_PRODUCTS = gql`
query GetProducts {
products {
id
name
description
price
}
}
`;
function ProductListContainer() {
const { loading, error, data } = useQuery<{ products: Product[] }>(GET_PRODUCTS);
if (loading) return <div>Loading products...</div>;
if (error) return <div>Error loading products: {error.message}</div>;
return <ProductList products={data?.products || []} />;
}
export default ProductListContainer;
The ProductListContainer
in the provided example epitomizes the concept of a Container component in React, especially within the context of the Container and Presentational pattern. Here's a brief explanation of its characteristics that classify it as a Container:
Data Management and Business Logic
Fetching Data: The
ProductListContainer
is responsible for fetching product data from a GraphQL endpoint. This is done using Apollo Client'suseQuery
hook. By managing the data fetching logic, the container encapsulates one of the primary aspects of business logic in an application.State Management: It handles various states associated with data fetching, such as
loading
,error
, anddata
. These states are crucial for managing the UI's response to different stages of the data fetching process (e.g., showing a loading indicator while the data is being fetched).
Separation of Concerns
No UI Rendering Logic: This component does not concern itself with how the data is presented in the UI. Instead, its sole focus is on fetching and managing the data and state. This clear separation of concerns adheres to the principle that Container components should not handle rendering the UI directly.
Passing Props to Presentational Components: The container passes the fetched data (in this case,
products
) to a Presentational component (ProductList
). This is a key characteristic of Container components – they provide data and callbacks as props to Presentational components, which then handle the rendering of the UI.
Reusability and Modularity
Modular Structure: The
ProductListContainer
encapsulates its functionality, making the component reusable and maintainable. If the data fetching or state management logic needs to be changed, it can be done within this container without affecting the Presentational components.Scalability: In a larger application, this approach allows for scalability, as the data management logic is abstracted away from the UI components. It becomes easier to manage and update the application's logic without touching the UI components.
Presentational Components
Characteristics:
Purely UI-focused: These components are primarily concerned with how things look on the screen. They should be written to be as dumb and stateless as possible, focusing only on the UI part.
Reusable and Composable: Since they don’t depend on the application's business logic, they can be easily reused across different parts of the app. This reusability is a significant advantage for scaling applications.
Props Driven: They receive all the data they need via props, making them predictable and easier to test. This includes callback functions they invoke, which are usually provided by container components.
ProductList (Presentational Component in TypeScript)
In the same e-commerce application, a ProductList
presentational component would receive the product data from ProductListContainer
and simply focus on how to display it. It could be a simple list or a grid layout, depending on the design requirements.
For the ProductList
component, we'll define a type for its props.
import React from 'react';
// Define the props type
type ProductListProps = {
products: {
id: number;
name: string;
description: string;
price: number;
}[];
};
function ProductList({ products }: ProductListProps) {
return (
<div>
<h2>Products</h2>
<ul>
{products.map(product => (
<li key={product.id}>
<h3>{product.name}</h3>
<p>{product.description}</p>
<strong>Price: ${product.price}</strong>
</li>
))}
</ul>
</div>
);
}
export default ProductList;
The ProductList
component in the example serves as an excellent illustration of a Presentational component in React. Its characteristics align with the core principles of the Presentational component pattern. Here's a breakdown of its key features:
Focus on Rendering UI
Purely for Presentation:
ProductList
is exclusively concerned with how the product list is displayed in the UI. It takes the data provided to it (in this case, the list of products) and renders it. This singular focus on presentation aligns perfectly with the role of Presentational components.No Business Logic: Unlike Container components,
ProductList
does not handle fetching data, managing application state, or any other form of business logic. It is only responsible for rendering the output of the data it receives.
Data Handling through Props
Props Driven: The component receives all the necessary data through its props, specifically the
products
array. This is a hallmark of Presentational components - they rely on data passed down to them, ensuring they remain decoupled from the data sourcing logic.Stateless Nature: In the given example,
ProductList
doesn't manage or maintain any internal state related to the application's data. It may have internal state for UI purposes (like toggling a dropdown within the list), but it remains independent of the application’s business logic.Real-world use cases for this pattern:
User Profile Page:
Presentational Component: Displays user information like name, photo, and bio. It's purely concerned with the UI and how the information is presented.
Container Component: Handles fetching user data from an API, managing state, and any logic for updating or editing the profile.
E-commerce Product List:
Presentational Component: A grid or list display of products, each product card showing an image, title, price, etc. This component is only concerned with how the products are displayed.
Container Component: Manages fetching the product data from a server, filtering products based on user input, and other business logic like pagination or sorting.
Dashboard Widgets:
Presentational Component: Individual widgets on a dashboard, like a chart, table, or summary card. These components are responsible for rendering data in a specific format.
Container Component: Fetches and processes the data needed for each widget, such as user analytics or sales figures, and passes it to the presentational components.
Forms:
Presentational Component: A generic form layout with various input fields, labels, and buttons. It focuses on the layout and style of the form elements.
Container Component: Handles form validation, submission, managing form state, and any interactions with backend services.
Blog or News Feed:
Presentational Component: Displays articles or news items with a consistent layout. It concerns itself with the presentation of the content (text, images, etc.).
Container Component: Deals with loading article data from a backend, managing pagination or infinite scroll, and possibly handling user interactions like likes or comments.
Navigation Menus:
Presentational Component: A component that defines the layout and style of a navigation menu, including menu items, dropdowns, etc.
Container Component: Manages the state of the menu (like which item is currently active), fetching any dynamic menu items, and handling user interactions like clicking.
Social Media Feed:
Presentational Component: Handles the layout and styling of posts, comments, likes, and other UI elements.
Container Component: Fetches the feed data, handles new post submissions, comment updates, and other interactive elements.
Shopping Cart:
Presentational Component: A UI component that shows the items in the cart, prices, and the total cost.
Container Component: Manages the state of the cart, updates quantities, removes items, and communicates with the backend for checkout processes.
Conclusion
By using the Container and Presentational components pattern, we've clearly separated the concerns of data fetching and state management from the UI rendering. The ProductListContainer
handles the logic, while the ProductList
is only concerned with how the product data is displayed. This separation makes the components more manageable, testable, and reusable.