Skip to content

Part 1.5 — Building a Small Interactive App: A Type-Safe Todo List

Site Console Site Console
4 min read Updated Nov 23, 2025 Web Development 0 comments
Build a Type-Safe Todo App in React with TypeScript

A Todo List may sound simple, but it’s the perfect sandbox to combine the essentials you’ve learned so far: components, props, events, state, controlled inputs, and data flow.
By the end of this post, you’ll have a compact, production-minded mini-app you can scale when we connect it to your NestJS backend later.


1. Plan the App (Like a Real Engineer)

Before you write code, outline what the app needs to do:

  1. Display a list of todos

  2. Allow adding new todos

  3. Allow toggling a todo as complete

  4. Allow deleting a todo

  5. Keep everything strongly typed

We’ll split functionality into clear, testable pieces:

  • A TodoItem component

  • A TodoList component

  • An AddTodoForm

  • A TodoApp wrapper that owns the state

This mirrors real-world frontend architecture.


2. Define the Todo Type

Every layer — frontend now, backend soon — should use clear contracts.
Start by defining a Todo type.

// src/types/todo.ts
export interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

Strong typing now prevents future drift when we add REST endpoints.


3. Build the AddTodoForm Component

This component handles user input and emits new todos upward.

// src/components/AddTodoForm.tsx
import { useState } from 'react';

interface AddTodoFormProps {
  onAdd: (text: string) => void;
}

export function AddTodoForm({ onAdd }: AddTodoFormProps) {
  const [text, setText] = useState('');

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    if (!text.trim()) return;
    onAdd(text.trim());
    setText('');
  };

  return (
    <form onSubmit={handleSubmit} className="flex gap-2 mt-4">
      <input
        className="border rounded p-2 flex-1"
        value={text}
        onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
          setText(e.target.value)
        }
        placeholder="Add a new todo..."
      />
      <button className="px-4 py-2 bg-blue-600 text-white rounded">
        Add
      </button>
    </form>
  );
}

This structure is predictable, testable, and scalable.


4. Build the TodoItem Component

A single todo needs to:

  • Render text

  • Toggle completion

  • Delete itself

// src/components/TodoItem.tsx
import type { Todo } from '../types/todo';

interface TodoItemProps {
  todo: Todo;
  onToggle: (id: number) => void;
  onDelete: (id: number) => void;
}

export function TodoItem({ todo, onToggle, onDelete }: TodoItemProps) {
  return (
    <li className="flex items-center justify-between py-1">
      <label className="flex items-center gap-2">
        <input
          type="checkbox"
          checked={todo.completed}
          onChange={() => onToggle(todo.id)}
        />
        <span className={todo.completed ? 'line-through text-gray-500' : ''}>
          {todo.text}
        </span>
      </label>

      <button
        onClick={() => onDelete(todo.id)}
        className="text-red-500 hover:underline"
      >
        Delete
      </button>
    </li>
  );
}

This component stays small and focused — ideal for testing.


5. Build the TodoList Component

This component receives todos and renders a list of <TodoItem />.

// src/components/TodoList.tsx
import type { Todo } from '../types/todo';
import { TodoItem } from './TodoItem';

interface TodoListProps {
  todos: Todo[];
  onToggle: (id: number) => void;
  onDelete: (id: number) => void;
}

export function TodoList({ todos, onToggle, onDelete }: TodoListProps) {
  if (todos.length === 0) {
    return <p className="mt-4 text-gray-500">No todos yet.</p>;
  }

  return (
    <ul className="mt-4 space-y-2">
      {todos.map(todo => (
        <TodoItem
          key={todo.id}
          todo={todo}
          onToggle={onToggle}
          onDelete={onDelete}
        />
      ))}
    </ul>
  );
}

This keeps rendering and data manipulation separate.


6. Build the Main TodoApp Component (State Lives Here)

This is where everything comes together.

// src/components/TodoApp.tsx
import { useState } from 'react';
import type { Todo } from '../types/todo';
import { AddTodoForm } from './AddTodoForm';
import { TodoList } from './TodoList';

export function TodoApp() {
  const [todos, setTodos] = useState<Todo[]>([]);

  const addTodo = (text: string) => {
    setTodos(prev => [
      ...prev,
      { id: Date.now(), text, completed: false }
    ]);
  };

  const toggleTodo = (id: number) => {
    setTodos(prev =>
      prev.map(todo =>
        todo.id === id
          ? { ...todo, completed: !todo.completed }
          : todo
      )
    );
  };

  const deleteTodo = (id: number) => {
    setTodos(prev => prev.filter(todo => todo.id !== id));
  };

  return (
    <section className="max-w-md mx-auto mt-10 p-6 bg-white rounded shadow">
      <h1 className="text-2xl font-bold">Todo List</h1>
      <AddTodoForm onAdd={addTodo} />
      <TodoList todos={todos} onToggle={toggleTodo} onDelete={deleteTodo} />
    </section>
  );
}

Every action (add, toggle, delete) is type-checked and predictable.


7. Render It in Your App

Inside your App.tsx:

import { TodoApp } from './components/TodoApp';

export default function App() {
  return (
    <main className="min-h-screen bg-gray-100">
      <TodoApp />
    </main>
  );
}

Run:

pnpm dev

You now have a complete Todo app.


8. Why This Matters for Full-Stack Work

In upcoming parts, you’ll replace local state with:

  • REST API calls to your NestJS backend

  • Prisma-backed PostgreSQL persistence

  • DTO validation for security

  • Jest + Playwright tests

This small app becomes the perfect testbed for real CRUD operations connected to your database.


9. Wrap-Up

You now know how to:

  • Combine components, props, events, and state

  • Design reusable UI structures

  • Build type-safe, predictable React features

  • Create a working interactive mini-application

In Part 1.6, we shift gears: you’ll learn how to test React components using Jest and React Testing Library — the preview for the deeper testing work coming in Part 5.

Related

Leave a comment

Sign in to leave a comment.

Comments