~/.claude/skills/ or any Agent-Skills-compatible runtime for AI-assisted Container work.
Container
- Smart wrappers that handle data and logic
- Separates data fetching from UI rendering
- Enables reusable and testable component architecture
Key Insight
Container components are smart wrappers that handle data and logic, while presentational components are dumb renderers that handle UI. This “brain vs beauty” separation means containers know about Redux, API calls, and business logic, while presentational components only know about props and rendering. The pattern prevents the common anti-pattern of mixing data fetching with JSX rendering, which creates components that are impossible to reuse or test in isolation.
Detailed Description
The Container/Presentational pattern (also called Smart/Dumb components) is a fundamental React architecture that separates concerns into two distinct component types.
Container Components (Smart Components):
- Purpose: Manage data, state, and side effects
- Responsibilities: Fetch data from APIs, connect to Redux store, handle business logic, manage local state, coordinate child components
- Characteristics: Usually contain state, lifecycle methods (or hooks), and logic. Minimal or no JSX markup.
- Naming Convention:
UserListContainer,TodoContainerComponent, or simplyUserList.container.js - Knowledge: Aware of Redux, routing, API structure, business rules
Presentational Components (Dumb Components):
- Purpose: Render UI based on props
- Responsibilities: Display data, emit callbacks when user interacts, handle styling and accessibility
- Characteristics: Stateless (functional components), receive data and callbacks via props, highly reusable
- Naming Convention:
UserList,TodoItem, or simplyUserList.js - Knowledge: Only aware of props and UI patterns
Why This Pattern Matters:
- Reusability: Presentational components can be reused with different data sources (Redux, REST API, GraphQL, local state, hardcoded data for Storybook)
- Testability: Presentational components are pure functions (props → UI) making them trivial to test. Containers isolate complex logic.
- Separation of Concerns: Data fetching bugs don’t affect UI, styling changes don’t break data flow
- Team Collaboration: Backend devs work on containers (API integration), frontend devs work on presentational components (UI)
- Storybook Integration: Presentational components make perfect Storybook stories since they don’t need API mocks
Modern React Context:
With React Hooks, the pattern has evolved. Custom hooks (like useUsers(), useTodos()) often replace containers, but the separation principle remains critical: components that fetch data should be separate from components that render data.
Code Examples
Basic Example: TodoListContainer across frameworks
The same container pulled from each chota-* template: it subscribes to the store, derives a filtered view of todos via selectors, and fans { todoData, events } into the presentational TodoList organism. Identical interface out the top, framework-native wiring down the side.
React + Redux
// templates/chota-react-redux/src/containers/TodoListContainer.jsx
// (The chota-react-rtk variant is identical — RTK's slice-generated
// actions plug into useDispatch the same way.)
import TodoList from "../ui/organisms/TodoList/TodoList.component";
import { useDispatch, useSelector } from "react-redux";
import {
createTodo, deleteTodo, editTodo, toggleTodo, updateTodo,
} from "../state/todo/todo.actions";
import { getSelectedFilter } from "../state/filters/filters.selectors";
import { getVisibleTodos } from "../state/todo/todo.selectors";
export default function TodoListContainer() {
const dispatch = useDispatch();
const selectedFilter = useSelector(getSelectedFilter);
const todoData = useSelector((state) =>
getVisibleTodos(state.todo, selectedFilter.id)
);
const events = {
onTodoCreate: (payload) => dispatch(createTodo(payload)),
onTodoEdit: (payload) => dispatch(editTodo(payload)),
onTodoUpdate: (text) =>
dispatch(updateTodo({ id: todoData.currentTodoItem.id, text })),
onTodoToggleUpdate: (id) => dispatch(toggleTodo(id)),
onTodoDelete: (payload) => dispatch(deleteTodo(payload)),
};
return <TodoList todoData={todoData} events={events} />;
}
React + Saga
// templates/chota-react-saga/src/containers/TodoListContainer.jsx
// Same as the Redux container, plus a useEffect that kicks off the
// initial READ_TODO dispatch — sagas pick that up and fetch from the API.
// The WC-Saga TodoListContainer does the same thing with a LitElement
// connected to the store via pwa-helpers.
import { useEffect } from "react";
import TodoList from "../ui/organisms/TodoList/TodoList.component";
import { useDispatch, useSelector } from "react-redux";
import {
createTodo, deleteTodo, editTodo, readTodo, toggleTodo, updateTodo,
} from "../state/todo/todo.actions";
import { getSelectedFilter } from "../state/filters/filters.selectors";
import { getVisibleTodos } from "../state/todo/todo.selectors";
export default function TodoListContainer() {
const dispatch = useDispatch();
const selectedFilter = useSelector(getSelectedFilter);
const todoData = useSelector((state) =>
getVisibleTodos(state.todo, selectedFilter.id)
);
useEffect(() => {
dispatch(readTodo());
}, [dispatch]);
const events = {
onTodoCreate: (payload) => dispatch(createTodo(payload)),
onTodoEdit: (payload) => dispatch(editTodo(payload)),
onTodoUpdate: (text) =>
dispatch(updateTodo({ id: todoData.currentTodoItem.id, text })),
onTodoToggleUpdate: (id) => dispatch(toggleTodo(id)),
onTodoDelete: (payload) => dispatch(deleteTodo(payload)),
};
return <TodoList todoData={todoData} events={events} />;
}
Angular + NgRx
// templates/chota-angular-ngrx/src/containers/TodoListContainer.ts
import { AsyncPipe } from '@angular/common';
import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { take } from 'rxjs/operators';
import { AppState } from '../app/state/index';
import { getVisibleTodos, getTodoState } from '../app/state/todo/todo.selectors';
import { Todo } from '../app/state/todo/todo.model';
import {
addTodoRequest, editTodo, updateTodoRequest, toggleTodo, deleteTodoRequest,
} from '../app/state/todo/todo.actions';
import TodoListComponent from '../ui/organisms/TodoList/TodoList.component';
@Component({
selector: 'app-todo-list-container',
standalone: true,
imports: [TodoListComponent, AsyncPipe],
template: `
@if (todoData$ | async; as todoData) {
<app-todo-list [todoData]="todoData" [events]="events"></app-todo-list>
}
`,
})
export default class TodoListContainerComponent {
todoData$ = this.store.select(getVisibleTodos);
events = {
onTodoCreate: (text: string) => this.store.dispatch(addTodoRequest({ text })),
onTodoEdit: (todo: Todo) =>
this.store.dispatch(editTodo({ id: todo.id, text: todo.text })),
onTodoUpdate: (text: string) => {
this.store.select(getTodoState).pipe(take(1)).subscribe((state) => {
const { id } = state.currentTodoItem;
if (id !== null) this.store.dispatch(updateTodoRequest({ id, text }));
});
},
onTodoToggleUpdate: (todo: Todo) =>
this.store.dispatch(toggleTodo({ id: todo.id })),
onTodoDelete: (id: number) =>
this.store.dispatch(deleteTodoRequest({ id })),
};
constructor(private store: Store<AppState>) {}
}
Vue + Pinia
<!-- templates/chota-vue-pinia/src/containers/TodoListContainer.vue -->
<template>
<TodoList :todoData="todoData.visibleTodos" :events="events" />
</template>
<script>
import { defineComponent } from 'vue';
import { useFiltersStore } from '../state/filters';
import { useTodoStore } from '../state/todo';
import TodoList from '../ui/organisms/TodoList/TodoList.component.vue';
export default defineComponent({
components: { TodoList },
setup() {
const filtersData = useFiltersStore();
const todoData = useTodoStore();
todoData.getTodos();
const events = {
onTodoCreate: (payload) => todoData.addTodos(payload),
onTodoEdit: (payload) => todoData.editTodo(payload),
onTodoUpdate: (text) =>
todoData.updateTodos({ id: todoData.currentTodoItem.id, text }),
onTodoToggleUpdate: (todo) => todoData.updateToggleTodos(todo),
onTodoDelete: (payload) => todoData.deleteTodos(payload),
};
return { filtersData, todoData, events };
},
});
</script>
Web Components + Saga
// templates/chota-wc-saga/src/containers/TodoListContainer.js
// LitElement + pwa-helpers `connect()` subscribes a custom element to the
// store. `stateChanged` is called on every dispatched action. Same events
// bag as the React-Saga container; same readTodo() kick-off on construct.
import { LitElement, html } from 'lit-element';
import { connect } from 'pwa-helpers';
import store, { useDispatch } from '../state';
import {
createTodo, deleteTodo, editTodo, readTodo, toggleTodo, updateTodo,
} from "../state/todo/todo.actions";
import { getSelectedFilter } from "../state/filters/filters.selectors";
import { getVisibleTodos } from "../state/todo/todo.selectors";
import "../ui/organisms/TodoList/app-todo-list";
export default class TodoListContainer extends connect(store)(LitElement) {
static get properties() {
return {
todoData: { type: Object },
selectedFilter: { type: Object },
events: { type: Object },
};
}
constructor() {
super();
const dispatch = useDispatch();
this.events = {
onTodoCreate: (e) => dispatch(createTodo(e.detail)),
onTodoEdit: (e) => dispatch(editTodo(e.detail)),
onTodoUpdate: (e) =>
dispatch(updateTodo({ id: this.todoData.currentTodoItem.id, text: e.detail })),
onTodoToggleUpdate: (e) => dispatch(toggleTodo(e.detail)),
onTodoDelete: (e) => dispatch(deleteTodo(e.detail)),
};
dispatch(readTodo());
}
stateChanged(state) {
this.selectedFilter = getSelectedFilter(state);
this.todoData = getVisibleTodos(state.todo, this.selectedFilter.id);
}
render() {
return html`<app-todo-list .todoData=${this.todoData} .events=${this.events}></app-todo-list>`;
}
}
The outward shape is always the same: a container component that selects from the store, builds an events callback bag, and renders the presentational organism with { todoData, events }. What changes:
- Subscription mechanism.
useSelector(React) /store.select(...).pipe(take(1))/ pwa-helpersconnect()withstateChanged/ Pinia’s direct store access insetup(). - Dispatch shape.
dispatch(action)in the Redux family;store.dispatch(...)in NgRx; store method calls (todoData.addTodos(...)) in Pinia; the samedispatch(action)in WC-Saga with the wrinkle that child events arrive asCustomEventso handlers reade.detail. - Initial fetch. Saga templates dispatch
readTodo()on mount (React’suseEffect, Lit’sconstructor); Pinia callstodoData.getTodos()directly insetup.
Practical Example: Modern React Hooks Approach
// ===== CUSTOM HOOK (Replaces Container) =====
// hooks/useTodos.js - Encapsulates data fetching logic
import { useState, useEffect } from 'react';
import { fetchTodos, updateTodo, deleteTodo } from '../api/todos';
export function useTodos() {
const [todos, setTodos] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
loadTodos();
}, []);
const loadTodos = async () => {
try {
setIsLoading(true);
setError(null);
const data = await fetchTodos();
setTodos(data);
} catch (err) {
setError(err.message);
} finally {
setIsLoading(false);
}
};
const toggleTodo = async (id) => {
const todo = todos.find(t => t.id === id);
if (!todo) return;
try {
const updated = await updateTodo(id, { completed: !todo.completed });
setTodos(todos.map(t => t.id === id ? updated : t));
} catch (err) {
console.error('Failed to update todo:', err);
}
};
const deleteTodoItem = async (id) => {
try {
await deleteTodo(id);
setTodos(todos.filter(t => t.id !== id));
} catch (err) {
console.error('Failed to delete todo:', err);
}
};
return {
todos,
isLoading,
error,
toggleTodo,
deleteTodoItem,
refetch: loadTodos
};
}
// ===== PRESENTATIONAL COMPONENT =====
// TodoList.js - Same as before, pure UI
function TodoList({ todos, onToggle, onDelete, isLoading, error }) {
// ... (same implementation as before)
}
// ===== PAGE COMPONENT (Uses Hook) =====
// pages/TodoPage.js - Composes hook + presentational component
import React from 'react';
import { useTodos } from '../hooks/useTodos';
import TodoList from '../components/TodoList';
function TodoPage() {
const { todos, isLoading, error, toggleTodo, deleteTodoItem } = useTodos();
return (
<div className="todo-page">
<h1>My Todos</h1>
<TodoList
todos={todos}
onToggle={toggleTodo}
onDelete={deleteTodoItem}
isLoading={isLoading}
error={error}
/>
</div>
);
}
export default TodoPage;
Advanced Example: Redux Container Pattern
// ===== PRESENTATIONAL COMPONENT =====
// UserList.js - Pure UI component
import React from 'react';
import PropTypes from 'prop-types';
function UserList({ users, selectedUserId, onSelectUser, onDeleteUser }) {
return (
<div className="user-list">
{users.map(user => (
<div
key={user.id}
className={`user-card ${selectedUserId === user.id ? 'selected' : ''}`}
onClick={() => onSelectUser(user.id)}>
<img src={user.avatar} alt={user.name} />
<h3>{user.name}</h3>
<p>{user.email}</p>
<button
onClick={(e) => {
e.stopPropagation();
onDeleteUser(user.id);
}}>
Delete
</button>
</div>
))}
</div>
);
}
UserList.propTypes = {
users: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
email: PropTypes.string.isRequired,
avatar: PropTypes.string.isRequired
})
).isRequired,
selectedUserId: PropTypes.number,
onSelectUser: PropTypes.func.isRequired,
onDeleteUser: PropTypes.func.isRequired
};
export default UserList;
// ===== REDUX ACTIONS =====
// actions/userActions.js
export const FETCH_USERS_REQUEST = 'FETCH_USERS_REQUEST';
export const FETCH_USERS_SUCCESS = 'FETCH_USERS_SUCCESS';
export const FETCH_USERS_FAILURE = 'FETCH_USERS_FAILURE';
export const SELECT_USER = 'SELECT_USER';
export const DELETE_USER = 'DELETE_USER';
export const fetchUsers = () => async (dispatch) => {
dispatch({ type: FETCH_USERS_REQUEST });
try {
const response = await fetch('/api/users');
const users = await response.json();
dispatch({ type: FETCH_USERS_SUCCESS, payload: users });
} catch (error) {
dispatch({ type: FETCH_USERS_FAILURE, payload: error.message });
}
};
export const selectUser = (userId) => ({
type: SELECT_USER,
payload: userId
});
export const deleteUser = (userId) => ({
type: DELETE_USER,
payload: userId
});
// ===== REDUX REDUCER =====
// reducers/userReducer.js
const initialState = {
users: [],
selectedUserId: null,
isLoading: false,
error: null
};
export default function userReducer(state = initialState, action) {
switch (action.type) {
case FETCH_USERS_REQUEST:
return { ...state, isLoading: true, error: null };
case FETCH_USERS_SUCCESS:
return { ...state, isLoading: false, users: action.payload };
case FETCH_USERS_FAILURE:
return { ...state, isLoading: false, error: action.payload };
case SELECT_USER:
return { ...state, selectedUserId: action.payload };
case DELETE_USER:
return {
...state,
users: state.users.filter(u => u.id !== action.payload),
selectedUserId: state.selectedUserId === action.payload ? null : state.selectedUserId
};
default:
return state;
}
}
// ===== CONTAINER COMPONENT (Redux Connect) =====
// containers/UserListContainer.js
import { connect } from 'react-redux';
import UserList from '../components/UserList';
import { fetchUsers, selectUser, deleteUser } from '../actions/userActions';
// mapStateToProps: Extract data from Redux store
const mapStateToProps = (state) => ({
users: state.users.users,
selectedUserId: state.users.selectedUserId,
isLoading: state.users.isLoading,
error: state.users.error
});
// mapDispatchToProps: Bind action creators
const mapDispatchToProps = {
onSelectUser: selectUser,
onDeleteUser: deleteUser,
fetchUsers
};
// Connected component
const UserListContainer = connect(
mapStateToProps,
mapDispatchToProps
)(UserList);
export default UserListContainer;
// ===== USAGE IN PAGE =====
// pages/UsersPage.js
import React, { useEffect } from 'react';
import { useDispatch } from 'react-redux';
import UserListContainer from '../containers/UserListContainer';
import { fetchUsers } from '../actions/userActions';
function UsersPage() {
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchUsers());
}, [dispatch]);
return (
<div className="users-page">
<h1>Users</h1>
<UserListContainer />
</div>
);
}
export default UsersPage;
// ===== MODERN REDUX TOOLKIT ALTERNATIVE =====
// hooks/useUsers.js - Using Redux Toolkit with hooks
import { useSelector, useDispatch } from 'react-redux';
import { useEffect } from 'react';
import { fetchUsers, selectUser, deleteUser } from '../slices/userSlice';
export function useUsers() {
const dispatch = useDispatch();
const users = useSelector(state => state.users.users);
const selectedUserId = useSelector(state => state.users.selectedUserId);
const isLoading = useSelector(state => state.users.isLoading);
const error = useSelector(state => state.users.error);
useEffect(() => {
dispatch(fetchUsers());
}, [dispatch]);
return {
users,
selectedUserId,
isLoading,
error,
selectUser: (id) => dispatch(selectUser(id)),
deleteUser: (id) => dispatch(deleteUser(id))
};
}
// Usage: Same presentational UserList component
// No container needed - just use the hook in the page
Common Mistakes
1. Mixing Data Fetching with Rendering
Mistake: Putting API calls and complex JSX in the same component.
// ❌ BAD: Component does too much
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// Data fetching mixed with rendering
async function loadData() {
const userData = await fetch(`/api/users/${userId}`).then(r => r.json());
const postsData = await fetch(`/api/users/${userId}/posts`).then(r => r.json());
setUser(userData);
setPosts(postsData);
setIsLoading(false);
}
loadData();
}, [userId]);
if (isLoading) return <div>Loading...</div>;
// Complex rendering logic
return (
<div className="user-profile">
<div className="user-header">
<img src={user.avatar} alt={user.name} />
<h1>{user.name}</h1>
<p>{user.bio}</p>
</div>
<div className="user-stats">
<span>Posts: {posts.length}</span>
<span>Followers: {user.followers}</span>
</div>
<div className="user-posts">
{posts.map(post => (
<article key={post.id}>
<h3>{post.title}</h3>
<p>{post.body}</p>
</article>
))}
</div>
</div>
);
}
// Can't reuse this component with different data sources
// Can't test UI without mocking fetch
// Can't use in Storybook without API
// ✅ GOOD: Separate container and presentational
// Custom hook (container logic)
function useUserProfile(userId) {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
async function loadData() {
const [userData, postsData] = await Promise.all([
fetch(`/api/users/${userId}`).then(r => r.json()),
fetch(`/api/users/${userId}/posts`).then(r => r.json())
]);
setUser(userData);
setPosts(postsData);
setIsLoading(false);
}
loadData();
}, [userId]);
return { user, posts, isLoading };
}
// Presentational component
function UserProfile({ user, posts, isLoading }) {
if (isLoading) return <div>Loading...</div>;
return (
<div className="user-profile">
<UserHeader user={user} />
<UserStats posts={posts} followers={user.followers} />
<PostList posts={posts} />
</div>
);
}
// Page component (uses hook)
function UserProfilePage({ userId }) {
const { user, posts, isLoading } = useUserProfile(userId);
return <UserProfile user={user} posts={posts} isLoading={isLoading} />;
}
// Now UserProfile is reusable and testable!
Why it matters: Mixed components are impossible to reuse, test, or document in Storybook.
2. Passing Too Many Individual Props
Mistake: Passing 10+ individual props instead of grouped objects.
// ❌ BAD: Props explosion
function UserCard({
id,
name,
email,
avatar,
bio,
location,
website,
followers,
following,
posts,
onFollow,
onMessage,
onBlock
}) {
return (
<div className="user-card">
{/* Using all these props... */}
</div>
);
}
// Container passes everything individually
function UserCardContainer({ userId }) {
const user = useUser(userId);
return (
<UserCard
id={user.id}
name={user.name}
email={user.email}
avatar={user.avatar}
bio={user.bio}
location={user.location}
website={user.website}
followers={user.followers}
following={user.following}
posts={user.posts}
onFollow={handleFollow}
onMessage={handleMessage}
onBlock={handleBlock}
/>
);
}
// ✅ GOOD: Group related data
function UserCard({ user, actions }) {
return (
<div className="user-card">
<img src={user.avatar} alt={user.name} />
<h2>{user.name}</h2>
<p>{user.bio}</p>
<button onClick={actions.onFollow}>Follow</button>
<button onClick={actions.onMessage}>Message</button>
</div>
);
}
UserCard.propTypes = {
user: PropTypes.shape({
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
email: PropTypes.string.isRequired,
avatar: PropTypes.string.isRequired,
bio: PropTypes.string
}).isRequired,
actions: PropTypes.shape({
onFollow: PropTypes.func.isRequired,
onMessage: PropTypes.func.isRequired,
onBlock: PropTypes.func.isRequired
}).isRequired
};
// Container is cleaner
function UserCardContainer({ userId }) {
const user = useUser(userId);
const actions = useUserActions(userId);
return <UserCard user={user} actions={actions} />;
}
Why it matters: Too many props make components hard to use and maintain. Grouped props improve readability.
3. Containers Rendering Too Much UI
Mistake: Container components containing complex JSX instead of delegating to presentational components.
// ❌ BAD: Container has complex UI
function ProductListContainer() {
const [products, setProducts] = useState([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetch('/api/products')
.then(r => r.json())
.then(data => {
setProducts(data);
setIsLoading(false);
});
}, []);
const handleAddToCart = (productId) => {
// Add to cart logic
};
// Container has too much UI logic
return (
<div className="product-list">
{isLoading ? (
<div className="loading-spinner">Loading...</div>
) : (
<div className="products-grid">
{products.map(product => (
<div key={product.id} className="product-card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p className="price">${product.price}</p>
<button onClick={() => handleAddToCart(product.id)}>
Add to Cart
</button>
</div>
))}
</div>
)}
</div>
);
}
// ✅ GOOD: Container delegates to presentational component
// Container: Data fetching only
function ProductListContainer() {
const { products, isLoading } = useProducts();
const { addToCart } = useCart();
return (
<ProductList
products={products}
isLoading={isLoading}
onAddToCart={addToCart}
/>
);
}
// Presentational: UI only
function ProductList({ products, isLoading, onAddToCart }) {
if (isLoading) {
return <LoadingSpinner />;
}
return (
<div className="products-grid">
{products.map(product => (
<ProductCard
key={product.id}
product={product}
onAddToCart={onAddToCart}
/>
))}
</div>
);
}
// Even more granular
function ProductCard({ product, onAddToCart }) {
return (
<div className="product-card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p className="price">${product.price}</p>
<button onClick={() => onAddToCart(product.id)}>
Add to Cart
</button>
</div>
);
}
Why it matters: Containers should orchestrate logic, not render UI. Keeping containers thin improves maintainability.