Flat Component Structure
App
├── Navbar
├── TodoList
├── TodoItem
├── AddTodo
└── FilterBar
Advantages:
- Debugging is straightforward because you can trace any issue directly to a component
- New developers can understand the entire app structure in minutes
- Changes are quick: want to add a new feature? Just add a new component
- Easy code reviews since components are self-contained
- Perfect for MVPs or proof of concepts
Disadvantages:
- Adding features like "undo" becomes messy because state is scattered
- Components become bloated as features grow (e.g., TodoItem handling edit, delete, complete, drag-n-drop)
- Sharing logic between components requires prop drilling or global state
- Hard to implement features that span multiple components (like offline sync)
- Performance optimizations become difficult (e.g., preventing unnecessary rerenders)
Container/Presenter Pattern
App
├── containers/
│ ├── TodoListContainer
│ ├── AddTodoContainer
│ └── FilterContainer
└── components/
├── TodoList
├── TodoItem
└── FilterBar
Advantages:
- Testing is easier because UI and logic are separate (test business logic without DOM)
- Designers can modify UI components without touching logic
- Reusing components across projects is simple (presentational components are just props + UI)
- Performance optimization is cleaner (container handles data, presenter handles rendering)
- Easier to swap out data sources (e.g., switching from REST to GraphQL)
Disadvantages:
- Simple features require two files (e.g., a basic button needs ButtonContainer and Button)
- More initial setup time (creating container/presenter pairs)
- File organization becomes important (where do shared containers go?)
- Can be overkill for simple UI elements
- Harder to understand for developers used to simple components
Feature-Based Modules
App
├── features/
│ ├── todos/
│ │ ├── components/
│ │ ├── hooks/
│ │ └── services/
│ └── auth/
│ ├── components/
│ ├── hooks/
│ └── services/
└── shared/
└── components/
Advantages:
- Teams can work on different features without conflicts
- Code splitting happens naturally by feature
- New developers can understand one feature without learning the entire app
- Features can be toggled on/off easily (e.g., for A/B testing)
- Clear boundaries make it easier to maintain consistent patterns within features
Disadvantages:
- Shared functionality needs careful planning (where does date formatting live?)
- More complex build setup needed for code splitting
- Risk of duplicating code between features
- Higher setup cost for new features
- Need strong conventions for sharing code between features
Real-world examples to illustrate:
Flat Structure Problem:
// TodoItem becoming a mess
function TodoItem({ todo }) {
const [isEditing, setIsEditing] = useState(false);
const [isDragging, setIsDragging] = useState(false);
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [editedText, setEditedText] = useState(todo.text);
const [isSaving, setIsSaving] = useState(false);
const [error, setError] = useState(null);
// Dozens of handlers
const handleEdit = async () => {/* ... */}
const handleDragStart = () => {/* ... */}
const handleDrop = () => {/* ... */}
const handleMenuOpen = () => {/* ... */}
const handleDelete = async () => {/* ... */}
const handleShare = async () => {/* ... */}
// etc...
}
Container/Presenter Solution:
// TodoItemContainer.tsx
function TodoItemContainer({ todoId }) {
const [isEditing, setIsEditing] = useState(false);
const { updateTodo, deleteTodo } = useTodoActions();
const handleEdit = async (newText: string) => {
await updateTodo(todoId, newText);
setIsEditing(false);
};
return (
<TodoItemPresenter
onEdit={handleEdit}
onDelete={() => deleteTodo(todoId)}
isEditing={isEditing}
setIsEditing={setIsEditing}
/>
);
}
// TodoItemPresenter.tsx
interface TodoItemPresenterProps {
onEdit: (text: string) => void;
onDelete: () => void;
isEditing: boolean;
setIsEditing: (editing: boolean) => void;
}
function TodoItemPresenter(props: TodoItemPresenterProps) {
// Only UI logic here
return (/* UI elements */);
}
Feature Module Example:
// features/todos/hooks/useTodoSync.ts
export function useTodoSync() {
const [syncStatus, setSyncStatus] = useState<SyncStatus>('idle');
useEffect(() => {
// Complex sync logic isolated to todos feature
const sync = async () => {
setSyncStatus('syncing');
await TodoSyncService.sync();
setSyncStatus('synced');
};
sync();
}, []);
return syncStatus;
}
// features/todos/components/TodoList.tsx
import { useTodoSync } from '../hooks/useTodoSync';
export function TodoList() {
const syncStatus = useTodoSync();
// Component logic
}
The choice often depends on practical factors:
- Team size (larger teams → feature modules)
- Timeline (tight deadline → flat structure)
- App complexity (complex state → container/presenter)
- Future maintenance (long-term project → feature modules)
- Team experience (junior team → flat structure)
A bad structure (just for fun)
├── navigation/
│ └── todos/
│ └── list/
│ └── item/
│ └── checkbox/
├── features/
│ └── todo-features/
│ └── todo-list-features/
│ └── todo-item-features/
├── state/
│ └── list-state/
│ └── item-state/
│ └── checkbox-state/
├── TodoList/
│ └── List/
│ └── Items/
│ └── Item/
├── data/
│ └── todos/
│ └── items/
│ ├── completed/
│ └── pending/
├── components/
│ ├── ListComponent/
│ │ └── TodoComponent/
│ │ └── ItemComponent/
│ └── TodoListWrapper/
│ └── TodoItemWrapper/
└── ui/
└── list/
└── item/
└── elements/
└── todo-elements/
What makes this hierarchy terrible?
- Everything is unnecessarily nested (checkbox inside item inside list inside todos)
- Redundant component folders doing the same thing
- Splitting related functionality across deeply nested folders
- State management split across multiple levels
- Similar components scattered across different folders
- Multiple folders serving the same purpose (TodoList vs List vs ListComponent)
- Data split into too many subfolders
- UI elements separated from their related components
- Features nested within features within features
- Navigation components mixed with actual components To add a simple "mark todo as complete" feature, you'd need to:
- Add logic in /state/list-state/item-state/checkbox-state
- Update UI in /ui/list/item/elements/todo-elements
- Modify component in /TodoList/List/Items/Item
- Update feature logic in /features/todo-features/todo-list-features/todo-item-features
- Change data structure in /data/todos/items/completed A simple task would require touching 5+ deeply nested folders!
Discuss
Which hierarchy is best for the simple to do list app? What added features or circumstances might make another hierarchy more viable?
Top comments (0)