FrontendAtlas
Interview Questions
Home>Vue.js interview questions> Vue coding challenges >Todo List (Refs + List Rendering)

Todo List (Refs + List Rendering)

easy
By FrontendAtlas Editorial · Updated Jul 16, 2026
easy level
Vue 3 Composition API with refs and computed List rendering with v-for and keys Immutable style array updates (map/filter) for clarity Basic component-level state management
Build a small Todo List in Vue 3 using the Composition API and <script setup>. Users should be able to add tasks, mark them as completed, remove individual tasks, and clear all completed tasks. Framework focus: Vue SFC patterns, ref/reactive state, computed/watch, and v-model bindings.
Vue 3 Composition API with refs and computedList rendering with v-for and keysImmutable style array updates (map/filter) for clarityBasic component-level state management

Requirements

  • Render a Todo List as the main UI in App.vue.
  • Use a text input and 'Add' button to create new todos.
  • Display todos in a list using v-for.
  • Each todo should have a checkbox to toggle completion and a 'Remove' button.
  • Provide a 'Clear completed' button that removes all completed todos when clicked.

Expected behavior

  • Clicking 'Add' with non-empty trimmed text adds a new todo to the list and clears the input.
  • Checking a todo's checkbox toggles its completed state and visually marks it (line-through).
  • Clicking 'Remove' deletes that specific todo.
  • Clicking 'Clear completed' removes all todos where done === true.
  • An empty state message is shown when there are no todos.

Implementation notes

  • Use ref('') for the controlled input value (newTask).
  • Store the list of todos in a ref<Todo[]>([]) where each todo has id, text, and done.
  • Use computed for simple derived state like hasTodos.
  • Implement helpers addTask, toggleTask, removeTask, and clearCompleted inside <script setup>.
  • Keep everything in a single App.vue component.

Solution

Overview

Use a single Vue 3 SFC with <script setup> to manage the todo list. Track the input value in newTask and store todos in a ref array with { id, text, done }. A computed property hasTodos drives the empty state and footer visibility. Actions (addTask, toggleTask, removeTask, clearCompleted) update the todos array, and the template uses v-for to render items plus a checkbox and Remove button.

**Key ideas**

  • Keep all state in refs inside <script setup>.
  • Use v-for + :key for list rendering.
  • Use immutable updates (map / filter) for clear toggling and removal.
  • Drive simple UI states (empty vs list, footer visibility) from computed values.
1

Single component with ref-backed list and immutable updates

Define type Todo = { id: number; text: string; done: boolean } and create const newTask = ref('') plus const todos = ref([] as Todo[]). addTask trims newTask.value, bails on empty, and appends a new todo with a unique id (e.g. Date.now()). toggleTask(id) replaces the array with todos.value.map(todo => todo.id === id ? { ...todo, done: !todo.done } : todo). removeTask(id) and clearCompleted() use filter to drop items. A hasTodos computed returns todos.value.length > 0 and controls whether you show the empty message or the list.

Files

{
  "name": "vue-todo-list-solution",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "vue": "3.x"
  },
  "scripts": {
    "serve": "vue-cli-service serve"
  }
}
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "Node",
    "strict": true,
    "allowJs": true
  }
}
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Vue SDK • Todo List (solution)</title></head><body><div id="app"></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;
}
<template>
  <main class="main">
    <section class="section">
      <h1>Todo List (Solution)</h1>

      <div class="panel">
        <div class="input-row">
          <input
            type="text"
            placeholder="Add a new task..."
            v-model="newTask"
          />
          <button type="button" class="primary" @click="addTask">
            Add
          </button>
        </div>

        <ul class="list" v-if="hasTodos">
          <li
            v-for="todo in todos"
            :key="todo.id"
            class="item"
          >
            <label class="item-main">
              <input
                type="checkbox"
                :checked="todo.done"
                @change="toggleTask(todo.id)"
              />
              <span :class="{ done: todo.done }">{{ todo.text }}</span>
            </label>

            <button
              type="button"
              class="ghost"
              @click="removeTask(todo.id)"
            >
              Remove
            </button>
          </li>
        </ul>

        <p v-else class="empty">No tasks yet. Add your first one above.</p>

        <div class="footer" v-if="hasTodos">
          <button
            type="button"
            class="ghost small"
            @click="clearCompleted"
          >
            Clear completed
          </button>
        </div>
      </div>
    </section>
  </main>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue';

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

// Controlled input
const newTask = ref('');

// List of todos (keep the type in a cast instead of a generic to avoid env issues)
const todos = ref([] as Todo[]);

// Derived flag
const hasTodos = computed(() => todos.value.length > 0);

// Add a new todo using newTask
// - Trim the text
// - Ignore empty strings
// - Push a new Todo with a unique id
// - Clear the input
const addTask = () => {
  if (!newTask.value.trim()) {
    return;
  }

  todos.value = [
    ...todos.value,
    {
      id: Date.now(),
      text: newTask.value.trim(),
      done: false
    }
  ];

  newTask.value = '';
};

// Toggle the "done" flag for a todo by id
const toggleTask = (id: number) => {
  todos.value = todos.value.map((todo) =>
    todo.id === id ? { ...todo, done: !todo.done } : todo
  );
};

// Remove a todo by id
const removeTask = (id: number) => {
  todos.value = todos.value.filter((todo) => todo.id !== id);
};

// Remove all todos where done === true
const clearCompleted = () => {
  todos.value = todos.value.filter((todo) => !todo.done);
};
</script>
import 'zone.js';
import { createApp } from 'vue';
import App from './App.vue';
import './App.css';

createApp(App).mount('#app');

Notes & Pitfalls

Pitfalls
  • Mutating todos in place without updating the ref value in a way that keeps the code clear.
  • Adding empty or whitespace-only tasks by forgetting to trim the input.
  • Forgetting to use `:key` in `v-for`, which can lead to odd checkbox behaviour.
  • Toggling completion without visually reflecting it (missing the `done` class binding).
Techniques
  • Using refs for both scalar and array state.
  • Using computed for derived flags such as `hasTodos`.
  • Updating arrays immutably to keep logic simple and predictable.
  • Binding classes dynamically with `:class` for completed todos.

Similar questions

Invite Chips Input (Tags + Autocomplete)intermediateMulti-step Signup Form (3-step Wizard)intermediateTransfer List (Move Selected Items Between Two Lists)intermediate

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 Vue.js 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 Vue Framework Prep Guide
↗Incidents hub
11 / 25