FrontendAtlas
Interview Questions
Home>React interview questions> React coding challenges >Todo List (Component with Local State)

Todo List (Component with Local State)

easy
By FrontendAtlas Editorial · Updated Jan 30, 2026
easy level
React `useState` for local state Controlled inputs Rendering lists with `map` Event handlers for click and change events Immutable updates for arrays of objects
Create a local-state todo list: add, toggle, and remove items. Keep input state controlled, generate stable ids, and update the todos array immutably so React can detect changes and re-render correctly. Framework focus: React hooks (useState/useEffect), JSX event handling, and one-way props/state flow.
React useState for local stateControlled inputsRendering lists with mapEvent handlers for click and change eventsImmutable updates for arrays of objects

Requirements

  • Render a Todo List UI as the main component.
  • Provide a text input and an 'Add' button to create new tasks.
  • Render a list of todos with a checkbox and label for each task.
  • When a task is checked, its text should appear with a line-through style.
  • Provide a 'Remove' button for each task.
  • Provide a 'Clear completed' button that removes all completed tasks.

Expected behavior

  • Typing into the input updates local component state.
  • Clicking 'Add' with non-empty text adds a new todo and clears the input.
  • Checking a todo toggles its completed state and updates the line-through style.
  • Clicking 'Remove' deletes that todo only.
  • Clicking 'Clear completed' removes all todos whose checkbox is checked.
  • The empty state message is shown when there are no todos.

Implementation notes

  • Use useState to manage the input value and the array of todo items.
  • Each todo item should at minimum have id, text, and done fields.
  • Use array methods like map and filter to update the todos immutably.
  • Use a derived boolean to decide when to show the empty state and footer.

Solution

Overview

Store the current input text and the list of todos in component state using useState. When the user clicks 'Add', trim the input, guard against empty strings, and append a new todo object to the array. Use map to toggle a todo's done flag, and filter to remove a specific todo or all completed todos.

**Key ideas**

  • Represent each todo with an object containing id, text, and done.
  • Treat the input as a controlled component.
  • Use immutable array updates (map, filter) when changing state.
  • Derive UI state, such as whether there are any todos, from the current array.
1

Single component with `useState` and immutable updates

Keep everything in a single App component. Use useState<string> for the input and useState<Todo[]> for the list. Implement helpers addTask, toggleTask, removeTask, and clearCompleted. Each helper uses immutable array operations: addTask spreads the previous array with a new item, toggleTask uses map to flip done for the matching id, removeTask and clearCompleted use filter to drop items. The checkbox checked prop is bound to todo.done, and the text element gets a done class when true.

Files

{
  "name": "react-todo-list-solution",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "react": "18.x",
    "react-dom": "18.x",
    "react-scripts": "5.0.1"
  },
  "scripts": { "start": "react-scripts start" }
}
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "target": "ES2020",
    "module": "ESNext",
    "strict": true
  }
}
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>React SDK • Todo List (solution)</title></head><body><div id="root"></div></body></html>
:root {
  color-scheme: dark;
}

html, body {
  margin: 0;
  height: 100%;
  background: #0a0a0a;
  font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
}

.main {
  min-height: 100vh;
  background: #0a0a0a;
  color: #e5e7eb;
  display: grid;
  place-items: center;
  padding: 24px;
}

.section {
  width: 100%;
  max-width: 480px;
  display: grid;
  gap: 16px;
}

h1 {
  margin: 0;
  font-size: 20px;
  font-weight: 700;
}

.panel {
  background: #0b1020;
  border: 1px solid #1f2937;
  border-radius: 12px;
  padding: 16px;
  display: grid;
  gap: 12px;
}

.input-row {
  display: flex;
  gap: 8px;
}

.input-row input {
  flex: 1;
  background: #020617;
  border-radius: 8px;
  border: 1px solid #1f2937;
  padding: 8px 10px;
  color: #e5e7eb;
  font-size: 14px;
}

.input-row input:focus {
  outline: none;
  border-color: #6366f1;
  box-shadow: 0 0 0 1px #6366f1;
}

.primary {
  background: #4ade80;
  color: #022c22;
  border: none;
  border-radius: 999px;
  padding: 8px 14px;
  font-weight: 600;
  font-size: 14px;
  cursor: pointer;
}

.primary[disabled] {
  opacity: .6;
  cursor: not-allowed;
}

.list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 6px;
}

.item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 6px 8px;
  border-radius: 8px;
  background: #020617;
  border: 1px solid transparent;
}

.item-main {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 14px;
}

.item-main input[type=checkbox] {
  accent-color: #22c55e;
}

.item-main span.done {
  text-decoration: line-through;
  color: #9ca3af;
}

button.ghost {
  background: transparent;
  color: #e5e7eb;
  border: 1px solid #374151;
  border-radius: 999px;
  padding: 6px 10px;
  font-size: 12px;
  cursor: pointer;
}

button.ghost.small {
  padding: 4px 10px;
  font-size: 12px;
}

.empty {
  margin: 4px 0 0;
  font-size: 13px;
  color: #9ca3af;
  text-align: center;
}

.footer {
  display: flex;
  justify-content: flex-end;
  margin-top: 4px;
}
import React, { useState } from 'react';
import './App.css';

type Todo = {
  id: number;
  text: string;
  done: boolean;
};

export default function App() {
  const [newTask, setNewTask] = useState('');
  // NOTE: no generic type argument here to avoid TS error in this environment
  const [todos, setTodos] = useState([]);

  const typedTodos = todos as Todo[];
  const hasTodos = typedTodos.length > 0;

  const addTask = () => {
    const text = newTask.trim();
    if (!text) {
      return;
    }

    setTodos((prev: any[]) => [
      ...prev,
      {
        id: Date.now(),
        text,
        done: false
      }
    ]);

    setNewTask('');
  };

  const toggleTask = (id: number) => {
    setTodos((prev: any[]) =>
      prev.map((todo: Todo) =>
        todo.id === id ? { ...todo, done: !todo.done } : todo
      )
    );
  };

  const removeTask = (id: number) => {
    setTodos((prev: any[]) => prev.filter((todo: Todo) => todo.id !== id));
  };

  const clearCompleted = () => {
    setTodos((prev: any[]) => prev.filter((todo: Todo) => !todo.done));
  };

  return (
    <main className="main">
      <section className="section">
        <h1>Todo List (Solution)</h1>

        <div className="panel">
          <div className="input-row">
            <input
              type="text"
              placeholder="Add a new task..."
              value={newTask}
              onChange={(event) => setNewTask(event.target.value)}
            />
            <button type="button" className="primary" onClick={addTask}>
              Add
            </button>
          </div>

          {hasTodos ? (
            <ul className="list">
              {typedTodos.map((todo) => (
                <li key={todo.id} className="item">
                  <label className="item-main">
                    <input
                      type="checkbox"
                      checked={todo.done}
                      onChange={() => toggleTask(todo.id)}
                    />
                    <span className={todo.done ? 'done' : ''}>{todo.text}</span>
                  </label>

                  <button
                    type="button"
                    className="ghost"
                    onClick={() => removeTask(todo.id)}
                  >
                    Remove
                  </button>
                </li>
              ))}
            </ul>
          ) : (
            <p className="empty">No tasks yet. Add your first one above.</p>
          )}

          {hasTodos && (
            <div className="footer">
              <button
                type="button"
                className="ghost small"
                onClick={clearCompleted}
              >
                Clear completed
              </button>
            </div>
          )}
        </div>
      </section>
    </main>
  );
}
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';

createRoot(document.getElementById('root')!).render(<App />);

Notes & Pitfalls

Pitfalls
  • Mutating the array in place instead of returning a new array.
  • Forgetting to clear the input after successfully adding a task.
  • Using uncontrolled inputs, which makes it harder to reset and test.
  • Forgetting to use a stable key when rendering the list.
Techniques
  • Using `useState` with functional updates when state depends on the previous value.
  • Using `map` / `filter` for immutable updates.
  • Deriving booleans like `hasTodos` from existing state instead of storing them separately.

Similar questions

React Dynamic Table (Rows × Columns)easyInvite Chips Input (Tags + Autocomplete)intermediateReact Filterable / Searchable User Listintermediate

Guides

Frontend interview preparation guideGuideFrontend coding interview questions and prep guideBlueprintJavaScript Problems That Actually Show UpBlueprintBuild Great UI in 60 MinutesBlueprint

Preparing for interviews? Use Frontend Coding Challenges first, then move into a concrete Study Plan before targeted Company Prep.

Open frontend interview questionsBrowse React interview questionsOpen Essential 60Open Machine Coding HubOpen Frontend Coding ChallengesOpen System DesignOpen Interview Prep GuideOpen System Design BlueprintOpen Framework Prep PathsOpen Study PlansOpen 30-day foundations study planOpen Company PrepOpen React Framework Prep Guide
↗Incidents hub
18 / 28