Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/rijvi-mahmud/shaddy/llms.txt

Use this file to discover all available pages before exploring further.

When a single component handles data fetching, state management, side effects, and UI rendering all at once, every layer becomes harder to change in isolation. The Container-Presentation pattern — sometimes called Smart-Dumb or Stateful-Stateless — draws a clear boundary: containers own the logic, presentational components own the pixels.

The Problem

Tightly coupled components are difficult to test and even harder to reuse. If a UserList component fetches its own data, you can’t render it in Storybook without mocking the network, and you can’t reuse the same UI with data from a different source without duplicating the fetch logic.

The Solution

Split components into two roles:
  • Container components — manage state, fetch data from APIs, handle side effects, and connect to global state stores.
  • Presentational components — accept data through props, call callback props for events, and focus entirely on rendering UI.

Container Component

A container component fetches and owns the data, then passes it down:
// UserListContainer.tsx
import { useState, useEffect } from 'react'
import { UserList } from './UserList'

interface User {
  id: number
  name: string
  email: string
}

export function UserListContainer() {
  const [users, setUsers] = useState<User[]>([])
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState<string | null>(null)

  useEffect(() => {
    fetchUsers()
  }, [])

  const fetchUsers = async () => {
    try {
      setLoading(true)
      const response = await fetch('/api/users')
      const data = await response.json()
      setUsers(data)
    } catch (err) {
      setError('Failed to fetch users')
    } finally {
      setLoading(false)
    }
  }

  const handleDeleteUser = async (id: number) => {
    try {
      await fetch(`/api/users/${id}`, { method: 'DELETE' })
      setUsers(users.filter((user) => user.id !== id))
    } catch (err) {
      setError('Failed to delete user')
    }
  }

  return (
    <UserList
      users={users}
      loading={loading}
      error={error}
      onDeleteUser={handleDeleteUser}
    />
  )
}

Presentational Component

The presentational component knows nothing about where the data comes from — it only renders what it receives:
// UserList.tsx
interface User {
  id: number
  name: string
  email: string
}

interface UserListProps {
  users: User[]
  loading: boolean
  error: string | null
  onDeleteUser: (id: number) => void
}

export function UserList({ users, loading, error, onDeleteUser }: UserListProps) {
  if (loading) {
    return <div className="text-center p-4">Loading users...</div>
  }

  if (error) {
    return <div className="text-red-500 p-4">{error}</div>
  }

  return (
    <div className="space-y-4">
      <h2 className="text-2xl font-bold">Users</h2>
      <ul className="divide-y divide-gray-200">
        {users.map((user) => (
          <li key={user.id} className="py-4 flex justify-between items-center">
            <div>
              <p className="font-medium">{user.name}</p>
              <p className="text-gray-600">{user.email}</p>
            </div>
            <button
              onClick={() => onDeleteUser(user.id)}
              className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
            >
              Delete
            </button>
          </li>
        ))}
      </ul>
    </div>
  )
}

One Container, Multiple Presentational Components

A container’s real power shows when it feeds multiple presentational components with the same data:
// DashboardContainer.tsx
import { useState, useEffect } from 'react'
import { UserList } from './UserList'
import { UserStats } from './UserStats'
import { UserFilter } from './UserFilter'

interface User {
  id: number
  name: string
  email: string
  status: 'active' | 'inactive'
  joinedAt: string
}

export function DashboardContainer() {
  const [users, setUsers] = useState<User[]>([])
  const [loading, setLoading] = useState(true)
  const [filter, setFilter] = useState<'all' | 'active' | 'inactive'>('all')

  useEffect(() => {
    fetch('/api/users')
      .then((res) => res.json())
      .then((data) => { setUsers(data); setLoading(false) })
  }, [])

  const filteredUsers = users.filter((user) =>
    filter === 'all' ? true : user.status === filter
  )

  const stats = {
    total: users.length,
    active: users.filter((u) => u.status === 'active').length,
    inactive: users.filter((u) => u.status === 'inactive').length,
  }

  const handleStatusChange = async (userId: number, status: string) => {
    await fetch(`/api/users/${userId}`, {
      method: 'PATCH',
      body: JSON.stringify({ status }),
    })
    // Refetch to keep UI in sync
  }

  return (
    <div className="space-y-6">
      <UserStats stats={stats} />
      <UserFilter currentFilter={filter} onFilterChange={setFilter} />
      <UserList
        users={filteredUsers}
        loading={loading}
        onStatusChange={handleStatusChange}
      />
    </div>
  )
}

Combining with Custom Hooks

For containers with complex logic, extract the data layer into custom hooks and keep the container as a thin orchestrator:
// hooks/useUserData.ts
import { useState, useEffect } from 'react'

export function useUserData() {
  const [users, setUsers] = useState([])
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    fetch('/api/users')
      .then((res) => res.json())
      .then((data) => { setUsers(data); setLoading(false) })
  }, [])

  return { users, loading, setUsers }
}
// DashboardContainer.tsx — now thin and easy to read
import { useUserData } from './hooks/useUserData'
import { UserList } from './UserList'
import { UserStats } from './UserStats'

export function DashboardContainer() {
  const { users, loading, setUsers } = useUserData()

  const stats = {
    total: users.length,
    active: users.filter((u) => u.status === 'active').length,
  }

  return (
    <div className="space-y-6">
      <UserStats stats={stats} />
      <UserList users={users} loading={loading} />
    </div>
  )
}

When to Use

  • Components are mixing significant business logic with rendering code.
  • You need to reuse the same UI with different data sources.
  • Multiple UI components on a page share the same fetched data.
  • You want to test the UI in isolation without network or state dependencies.
  • Multiple team members are working on the same feature.

When Not to Use

  • The component is simple and doesn’t contain complex logic.
  • You only need to render a single UI component with no sharing.
  • You’re prototyping rapidly and the overhead of extra files slows you down.
  • The component won’t be reused or composed with others.

Pros and Cons

ProsCons
SeparationLogic and UI change independentlyMore files per feature
TestabilityTest UI with plain props; test logic in hooksRequires discipline to maintain the boundary
ReusabilitySame UI with different containersOver-applied to simple components, adds noise
CollaborationDesigners and developers can work in parallelNaming conventions must be agreed upon
Name containers consistently — UserListContainer and UserList, or UserListPage and UserListView. A clear naming convention makes the pattern obvious to new contributors.

Build docs developers (and LLMs) love