DEV Community

Kevklatman
Kevklatman

Posted on

Three valid ways to structure your simple to do list app (and one stupid one).

Flat Component Structure

App
├── Navbar
├── TodoList
├── TodoItem
├── AddTodo
└── FilterBar
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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/
Enter fullscreen mode Exit fullscreen mode

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...
}
Enter fullscreen mode Exit fullscreen mode

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 */);
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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/

Enter fullscreen mode Exit fullscreen mode

What makes this hierarchy terrible?

  1. Everything is unnecessarily nested (checkbox inside item inside list inside todos)
  2. Redundant component folders doing the same thing
  3. Splitting related functionality across deeply nested folders
  4. State management split across multiple levels
  5. Similar components scattered across different folders
  6. Multiple folders serving the same purpose (TodoList vs List vs ListComponent)
  7. Data split into too many subfolders
  8. UI elements separated from their related components
  9. Features nested within features within features
  10. Navigation components mixed with actual components To add a simple "mark todo as complete" feature, you'd need to:
  11. Add logic in /state/list-state/item-state/checkbox-state
  12. Update UI in /ui/list/item/elements/todo-elements
  13. Modify component in /TodoList/List/Items/Item
  14. Update feature logic in /features/todo-features/todo-list-features/todo-item-features
  15. 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)