React, Angular and Vue are the three best-known ways to build interactive web front ends. Comparisons often stay abstract, so this one is concrete: we built the same todo component in all three, with the same features and the same text on screen, then compiled and tested each one. Below you will find the code, what is different about each framework, a comparison table and a simple guide to choosing.
Versions and facts in this guide were checked against official documentation on 26 October 2026 (IST). Tools move fast, so confirm version numbers on the linked official pages before you install.
Current versions at a glance
| Framework | Latest major (26 Oct 2026) | Maintained by | Release rhythm |
|---|---|---|---|
| React | 19 (19.3 released 9 September 2026) | Meta and the open-source community | No fixed calendar; minor releases ship when ready |
| Angular | 22 (released 3 June 2026; 22.2 is the latest minor) | Now one major release every 12 months (it was every 6 months until v22); v21 and v20 are in long-term support | |
| Vue | 3 (3.5.x on npm) | Evan You and the Vue core team | Minor releases within Vue 3 |
Sources: react.dev/versions, angular.dev's release page and the Vue quick-start guide plus the npm registry (links at the end). Angular's own release page lists v23 for around June 2027.
The test component: a small todo list
Each version does the same five things: type a task and press Add (blank input is ignored), show the list, tick a checkbox to strike a task through, delete a task, and show "N of M left". The goal is to show each framework's core ideas: state, event handling, two-way input binding, list rendering and derived values.
React 19 version
Create a project. React's documentation recommends starting with a framework such as Next.js or React Router, or building from scratch with a tool like Vite. For a single component, Vite is the lightest option:
npm create vite@latest react-todo -- --template react-ts
cd react-todo
npm install
npm run devThen add src/TodoList.tsx:
// TodoList.tsx (React 19)
import { useState } from "react";
import type { FormEvent } from "react";
type Todo = { id: number; text: string; done: boolean };
export default function TodoList() {
const [todos, setTodos] = useState<Todo[]>([]);
const [text, setText] = useState("");
const remaining = todos.filter((t) => !t.done).length;
function addTodo(e: FormEvent) {
e.preventDefault();
const value = text.trim();
if (!value) return;
setTodos([...todos, { id: Date.now(), text: value, done: false }]);
setText("");
}
function toggle(id: number) {
setTodos(todos.map((t) => (t.id === id ? { ...t, done: !t.done } : t)));
}
function remove(id: number) {
setTodos(todos.filter((t) => t.id !== id));
}
return (
<section>
<form onSubmit={addTodo}>
<input
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="What needs doing?"
/>
<button type="submit">Add</button>
</form>
<ul>
{todos.map((t) => (
<li key={t.id}>
<input type="checkbox" checked={t.done} onChange={() => toggle(t.id)} />
<span style={{ textDecoration: t.done ? "line-through" : "none" }}>{t.text}</span>
<button onClick={() => remove(t.id)}>Delete</button>
</li>
))}
</ul>
<p>{remaining} of {todos.length} left</p>
</section>
);
}What to notice: React components are plain functions that return JSX (HTML-like syntax inside TypeScript). State lives in useState, and you never change it in place. Instead you pass a new array to setTodos, which is why the code uses [...todos, newItem], map and filter. The input is "controlled": its value comes from state and onChange writes back to it. remaining is just a normal variable recalculated on every render.
Vue 3 version
Vue's official scaffolding tool is create-vue (it asks a few questions; choose TypeScript):
npm create vue@latest
cd vue-todo
npm install
npm run devThen add src/components/TodoList.vue, a single-file component:
<!-- TodoList.vue (Vue 3, Composition API) -->
<script setup lang="ts">
import { ref, computed } from "vue";
type Todo = { id: number; text: string; done: boolean };
const todos = ref<Todo[]>([]);
const text = ref("");
const remaining = computed(() => todos.value.filter((t) => !t.done).length);
function addTodo() {
const value = text.value.trim();
if (!value) return;
todos.value.push({ id: Date.now(), text: value, done: false });
text.value = "";
}
function remove(id: number) {
todos.value = todos.value.filter((t) => t.id !== id);
}
</script>
<template>
<section>
<form @submit.prevent="addTodo">
<input v-model="text" placeholder="What needs doing?" />
<button type="submit">Add</button>
</form>
<ul>
<li v-for="t in todos" :key="t.id">
<input type="checkbox" v-model="t.done" />
<span :style="{ textDecoration: t.done ? 'line-through' : 'none' }">{{ t.text }}</span>
<button @click="remove(t.id)">Delete</button>
</li>
</ul>
<p>{{ remaining }} of {{ todos.length }} left</p>
</section>
</template>What to notice: a single-file component keeps script, template and (optionally) styles in one .vue file. ref() creates reactive state, and Vue tracks changes automatically, so todos.value.push(...) just works. v-model gives two-way binding for both the text box and the checkbox, so no separate toggle function is needed. computed() caches the remaining count. @submit.prevent is shorthand for calling preventDefault().
Angular 22 version
Install the Angular CLI and create a project (the Angular install guide asks for a recent Node.js; check the exact minimum on angular.dev before installing):
npm install -g @angular/cli
ng new ng-todo
cd ng-todo
npm startThen add src/app/todo-list.ts:
// todo-list.ts (Angular 22, standalone component with signals)
import { Component, computed, signal } from "@angular/core";
import { FormsModule } from "@angular/forms";
type Todo = { id: number; text: string; done: boolean };
@Component({
selector: "app-todo-list",
imports: [FormsModule],
template: `
<section>
<form (ngSubmit)="addTodo()">
<input name="text" [(ngModel)]="text" placeholder="What needs doing?" />
<button type="submit">Add</button>
</form>
<ul>
@for (t of todos(); track t.id) {
<li>
<input type="checkbox" [checked]="t.done" (change)="toggle(t.id)" />
<span [style.text-decoration]="t.done ? 'line-through' : 'none'">{{ t.text }}</span>
<button type="button" (click)="remove(t.id)">Delete</button>
</li>
}
</ul>
<p>{{ remaining() }} of {{ todos().length }} left</p>
</section>
`,
})
export class TodoList {
todos = signal<Todo[]>([]);
text = signal("");
remaining = computed(() => this.todos().filter((t) => !t.done).length);
addTodo() {
const value = this.text().trim();
if (!value) return;
this.todos.update((list) => [...list, { id: Date.now(), text: value, done: false }]);
this.text.set("");
}
toggle(id: number) {
this.todos.update((list) => list.map((t) => (t.id === id ? { ...t, done: !t.done } : t)));
}
remove(id: number) {
this.todos.update((list) => list.filter((t) => t.id !== id));
}
}And render it from the root component, src/app/app.ts:
import { Component } from '@angular/core';
import { TodoList } from './todo-list';
@Component({
imports: [TodoList],
selector: 'app-root',
template: '<app-todo-list />',
})
export class App {}What to notice: Angular components are TypeScript classes with a @Component decorator. Components are standalone by default in modern Angular, so you list what the template needs in imports (here FormsModule for ngModel). State is held in signals: read with todos(), change with .set() or .update(), and derive values with computed(). Templates use the built-in @for block with a required track expression. [(ngModel)] is two-way binding, (event) listens and [property] binds.
How we tested the three versions
- React: React 19.3.1 with Vite 8 and TypeScript.
npm run buildpassed, and a Vitest + Testing Library test typed a task, tried a blank task, ticked the checkbox and deleted the item, checking "1 of 1 left", "0 of 1 left" and "0 of 0 left". - Vue: Vue 3.5.44 with Vite and
vue-tsctype checking. The build and an equivalent Vitest test passed. - Angular: Angular CLI 22 on Node.js 24.
ng buildpassed andng test(Vitest, the default test runner in new Angular projects) passed with the same scenario.
One Angular testing tip from this exercise: new Angular apps are zoneless by default. In a unit test, after you dispatch an input event, call fixture.detectChanges() and await fixture.whenStable() before you assert, or the view will not have updated yet.
Side-by-side comparison
| React | Angular | Vue | |
|---|---|---|---|
| What it is | UI library; you add routing, data fetching, etc. (or use a framework like Next.js) | Full framework: router, forms, HTTP client, testing and CLI included | Progressive framework: core library plus official router (Vue Router) and state (Pinia) |
| Template style | JSX inside TypeScript/JavaScript | HTML templates with Angular syntax (@for, [ ], ( )) | HTML templates with directives (v-for, v-model) |
| State in our example | useState with new arrays (immutable updates) | Signals (signal, computed) | Reactive refs (ref, computed), mutation allowed |
| Two-way input binding | Manual: value + onChange | [(ngModel)] | v-model |
| TypeScript | Optional, widely used | TypeScript-first | Optional, well supported |
| Learning curve | Small core, many ecosystem choices | Steepest at first, very consistent afterwards | Generally considered the gentlest |
| Typical users | Startups to big tech; huge ecosystem (Next.js, React Native) | Large enterprise apps and teams that want one standard way | Small to large apps, popular for adding interactivity to existing sites |
| Starter command | npm create vite@latest or a framework CLI | ng new | npm create vue@latest |
When to pick which
Choose React if...
You want the largest ecosystem and job pool for front-end work, you like writing UI as JavaScript functions, or you may later build mobile apps with React Native. Be ready to choose extra libraries (or adopt Next.js or React Router as a framework).
Choose Angular if...
You are joining or building a large, long-lived application with many developers, you like strong conventions, and you want routing, forms, HTTP and testing to come from one place with one CLI. Angular's yearly major releases and LTS windows suit enterprise planning.
Choose Vue if...
You want the quickest path from HTML knowledge to a working app, you like single-file components, or you are adding interactive parts to an existing server-rendered site.
New to programming altogether? Read Python vs JavaScript vs Java first. All three frameworks assume comfortable JavaScript. Keep each project in version control from the first commit (see Git and GitHub for beginners), and when your front end needs data, connect it to a backend like the one in REST APIs explained with Flask, Express and fetch.
FAQ
Which is best for beginners?
Vue is often considered the gentlest because its templates look like HTML. React has the smallest core API. Angular asks you to learn the most up front. Any of the three is a fine first framework once you know basic JavaScript.
Do I need TypeScript?
Not for React or Vue, though both work well with it and our examples use it. Angular is built around TypeScript, so you will use it there.
Is Angular the same as AngularJS?
No. AngularJS (1.x) is the original framework and is no longer supported. Modern Angular is a complete rewrite and is what this article covers.
Can I use these with any backend?
Yes. All three run in the browser and talk to any backend over HTTP, usually a JSON REST API.