FossilsšŸ› ļø Machine CodingBuild a Todo List with Full CRUD
🐣HatchlingMachine CodingCRUDState Management

Build a Todo List with Full CRUD

The Todo list sounds simple but interviewers use it to test state management patterns, data persistence, filtering, and clean component architecture.

Build a Todo List with Full CRUD

Interview Question: "Build a Todo app with add, edit, delete, toggle complete, filter by status, and local storage persistence."

Requirements

  • Add new todos
  • Edit todo text inline
  • Toggle complete/incomplete
  • Delete todos
  • Filter: All / Active / Completed
  • Persist to localStorage
  • Bulk actions: Clear completed, Toggle all

Data Model

interface Todo {
  id: string;
  text: string;
  completed: boolean;
  createdAt: number;
}
 
type Filter = 'all' | 'active' | 'completed';

React Implementation

function useTodos() {
  const [todos, setTodos] = useLocalStorage<Todo[]>('todos', []);
  const [filter, setFilter] = useState<Filter>('all');
 
  const addTodo = useCallback((text: string) => {
    if (!text.trim()) return;
    setTodos(prev => [
      ...prev,
      { id: crypto.randomUUID(), text: text.trim(), completed: false, createdAt: Date.now() },
    ]);
  }, [setTodos]);
 
  const toggleTodo = useCallback((id: string) => {
    setTodos(prev => prev.map(t => t.id === id ? { ...t, completed: !t.completed } : t));
  }, [setTodos]);
 
  const editTodo = useCallback((id: string, text: string) => {
    setTodos(prev => prev.map(t => t.id === id ? { ...t, text } : t));
  }, [setTodos]);
 
  const deleteTodo = useCallback((id: string) => {
    setTodos(prev => prev.filter(t => t.id !== id));
  }, [setTodos]);
 
  const clearCompleted = useCallback(() => {
    setTodos(prev => prev.filter(t => !t.completed));
  }, [setTodos]);
 
  const toggleAll = useCallback(() => {
    setTodos(prev => {
      const allCompleted = prev.every(t => t.completed);
      return prev.map(t => ({ ...t, completed: !allCompleted }));
    });
  }, [setTodos]);
 
  const filteredTodos = useMemo(() => {
    switch (filter) {
      case 'active': return todos.filter(t => !t.completed);
      case 'completed': return todos.filter(t => t.completed);
      default: return todos;
    }
  }, [todos, filter]);
 
  const activeCount = useMemo(() => todos.filter(t => !t.completed).length, [todos]);
 
  return {
    todos: filteredTodos,
    activeCount,
    filter,
    setFilter,
    addTodo,
    toggleTodo,
    editTodo,
    deleteTodo,
    clearCompleted,
    toggleAll,
  };
}

TodoItem with Inline Edit

function TodoItem({
  todo,
  onToggle,
  onEdit,
  onDelete,
}: {
  todo: Todo;
  onToggle: () => void;
  onEdit: (text: string) => void;
  onDelete: () => void;
}) {
  const [isEditing, setIsEditing] = useState(false);
  const [editText, setEditText] = useState(todo.text);
  const inputRef = useRef<HTMLInputElement>(null);
 
  useEffect(() => {
    if (isEditing) inputRef.current?.focus();
  }, [isEditing]);
 
  const handleSubmit = () => {
    const trimmed = editText.trim();
    if (trimmed) {
      onEdit(trimmed);
      setIsEditing(false);
    } else {
      onDelete();
    }
  };
 
  if (isEditing) {
    return (
      <li className="todo-item editing">
        <input
          ref={inputRef}
          value={editText}
          onChange={e => setEditText(e.target.value)}
          onBlur={handleSubmit}
          onKeyDown={e => {
            if (e.key === 'Enter') handleSubmit();
            if (e.key === 'Escape') {
              setEditText(todo.text);
              setIsEditing(false);
            }
          }}
        />
      </li>
    );
  }
 
  return (
    <li className={`todo-item ${todo.completed ? 'completed' : ''}`}>
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={onToggle}
        aria-label={`Mark "${todo.text}" as ${todo.completed ? 'incomplete' : 'complete'}`}
      />
      <span onDoubleClick={() => setIsEditing(true)}>{todo.text}</span>
      <button onClick={onDelete} aria-label={`Delete "${todo.text}"`}>Ɨ</button>
    </li>
  );
}

App Shell

function TodoApp() {
  const {
    todos, activeCount, filter, setFilter,
    addTodo, toggleTodo, editTodo, deleteTodo,
    clearCompleted, toggleAll,
  } = useTodos();
  const [input, setInput] = useState('');
 
  return (
    <div className="todo-app">
      <form onSubmit={e => { e.preventDefault(); addTodo(input); setInput(''); }}>
        <input
          value={input}
          onChange={e => setInput(e.target.value)}
          placeholder="What needs to be done?"
          autoFocus
        />
      </form>
      {todos.length > 0 && (
        <>
          <button onClick={toggleAll}>Toggle All</button>
          <ul className="todo-list">
            {todos.map(todo => (
              <TodoItem
                key={todo.id}
                todo={todo}
                onToggle={() => toggleTodo(todo.id)}
                onEdit={text => editTodo(todo.id, text)}
                onDelete={() => deleteTodo(todo.id)}
              />
            ))}
          </ul>
          <footer>
            <span>{activeCount} item{activeCount !== 1 ? 's' : ''} left</span>
            <div role="radiogroup" aria-label="Filter">
              {(['all', 'active', 'completed'] as Filter[]).map(f => (
                <button
                  key={f}
                  role="radio"
                  aria-checked={filter === f}
                  className={filter === f ? 'selected' : ''}
                  onClick={() => setFilter(f)}
                >
                  {f.charAt(0).toUpperCase() + f.slice(1)}
                </button>
              ))}
            </div>
            <button onClick={clearCompleted}>Clear Completed</button>
          </footer>
        </>
      )}
    </div>
  );
}

What Interviewers Look For

  1. Data flow — Unidirectional state management, immutable updates
  2. Persistence — localStorage sync without race conditions
  3. Editing UX — Double-click to edit, Enter to save, Escape to cancel, blur to save
  4. Performance — useCallback for stable references, useMemo for filtered list
  5. Edge cases — Empty text prevention, trim whitespace, delete on empty edit