First React App - Build a Counter & Todo List!

Build your first real React apps! We'll make a counter and a todo list from scratch, step by step!

Introduction

Time to build! We'll make two classic beginner apps: a counter and a todo list! Let's go!

What You Will Learn

  • Using useState hook
  • Handling events
  • Rendering lists
  • Basic React patterns

Prerequisites

Project 1: React Counter

Let's start simple!

Step 1: Create the Counter Component

In src/App.jsx:

import { useState } from 'react';

function Counter() {
  // Declare a state variable called count, starting at 0
  const [count, setCount] = useState(0);

  return (
    <div style={{ padding: '2rem', textAlign: 'center' }}>
      <h1>React Counter</h1>
      <h2 style={{ fontSize: '3rem' }}>{count}</h2>
      <div>
        <button 
          onClick={() => setCount(count - 1)}
          style={{ fontSize: '1.5rem', padding: '0.5rem 1rem', margin: '0.5rem' }}
        >
          -
        </button>
        <button 
          onClick={() => setCount(0)}
          style={{ fontSize: '1.5rem', padding: '0.5rem 1rem', margin: '0.5rem' }}
        >
          Reset
        </button>
        <button 
          onClick={() => setCount(count + 1)}
          style={{ fontSize: '1.5rem', padding: '0.5rem 1rem', margin: '0.5rem' }}
        >
          +
        </button>
      </div>
    </div>
  );
}

export default Counter;

Step 2: Run It!

Save and visit http://localhost:5173! You have a working counter!

Project 2: Todo List (More Advanced!)

Now let's build something cooler! A todo list!

Step 1: Create Todo Component

Replace App.jsx:

import { useState } from 'react';

function TodoApp() {
  // State for todos array and new todo input
  const [todos, setTodos] = useState([]);
  const [newTodo, setNewTodo] = useState('');

  // Handle form submission
  function handleSubmit(e) {
    e.preventDefault();
    if (newTodo.trim()) {
      setTodos([...todos, { id: Date.now(), text: newTodo, done: false }]);
      setNewTodo('');
    }
  }

  // Toggle todo done/undone
  function toggleTodo(id) {
    setTodos(todos.map(todo =>
      todo.id === id ? { ...todo, done: !todo.done } : todo
    ));
  }

  // Delete a todo
  function deleteTodo(id) {
    setTodos(todos.filter(todo => todo.id !== id));
  }

  return (
    <div style={{ maxWidth: '400px', margin: '2rem auto', padding: '0 1rem' }}>
      <h1 style={{ textAlign: 'center' }}>Todo List</h1>
      
      {/* Form to add new todo */}
      <form onSubmit={handleSubmit} style={{ display: 'flex', gap: '0.5rem', marginBottom: '1rem' }}>
        <input 
          type="text"
          value={newTodo}
          onChange={(e) => setNewTodo(e.target.value)}
          placeholder="Add a new todo..."
          style={{ flex: 1, padding: '0.5rem', fontSize: '1rem' }}
        />
        <button type="submit" style={{ padding: '0.5rem 1rem' }}>
          Add
        </button>
      </form>

      {/* Todo list */}
      <ul style={{ listStyle: 'none', padding: 0 }}>
        {todos.map(todo => (
          <li 
            key={todo.id}
            style={{
              display: 'flex',
              justifyContent: 'space-between',
              alignItems: 'center',
              padding: '0.75rem',
              marginBottom: '0.5rem',
              backgroundColor: '#f0f0f0',
              borderRadius: '4px',
              textDecoration: todo.done ? 'line-through' : 'none',
              opacity: todo.done ? 0.7 : 1
            }}
          >
            <span onClick={() => toggleTodo(todo.id)} style={{ cursor: 'pointer' }}>
              {todo.text}
            </span>
            <button 
              onClick={() => deleteTodo(todo.id)}
              style={{ color: 'red', border: 'none', background: 'transparent', cursor: 'pointer' }}
            >
              ✕
            </button>
          </li>
        ))}
      </ul>

      {/* Empty state */}
      {todos.length === 0 && <p style={{ textAlign: 'center', color: '#888' }}>No todos yet! Add one above!</p>}
    </div>
  );
}

export default TodoApp;

Key Concepts We Used

1. useState

We used useState for:

  • Counter value
  • Todos array
  • New todo input

2. Event Handlers

  • onClick for buttons
  • onSubmit for forms
  • onChange for inputs

3. Lists & Keys

We used map with unique keys!

4. Immutability

We never mutated state directly! We always used setters and spread operators!

Best Practices

  1. Keep components small: We could split TodoList into smaller components!
  2. Use descriptive variable names: todos not t!
  3. Always use keys with map: Helps React keep track!
  4. Handle empty states: We showed a message when there are no todos!

Common Mistakes

  1. Mutating state directly: Never do todos.push(...) - always use setTodos!
  2. Not preventing default form behavior: We added e.preventDefault()!
  3. Forgetting keys: Always add keys when mapping!

Next Steps

From here, you could add:

  • Edit todos
  • Filter todos (All/Active/Completed)
  • LocalStorage persistence
  • Due dates

Summary

You just built two React apps! A counter and a todo list! You used useState, event handlers, and lists - the building blocks of React!

Related Articles

Previous Tutorial

React Ecosystem

Next Tutorial

Let's dive deep into JSX! → Introduction to JSX