React vs Vue vs Angular: Same App in 14 Steps [2026]

Picking a front-end framework in August 2026 usually turns into a scroll through opinion pieces that repeat the same three bullet points: React has the biggest ecosystem, Vue is the easiest to pick up, Angular is what enterprises standardize on. None of that tells you what actually happens when you sit down and build something. This tutorial skips the theory and walks through building the identical small app — a todo list that fetches data from a live REST API — three times: once in React 19.2 with Vite, once in Vue 3.5 with the official create-vue scaffolder, and once in Angular 22 with standalone components and signals.

By the end you’ll have three working projects, a side-by-side look at bundle size and dev-server speed, working test suites, and a production build for each stack. The react vs vue vs angular debate stops being abstract once you’ve typed the same feature into all three component models back to back. Total time: roughly 150 minutes if you follow every step end to end, less if you only build one or two of the three stacks.

This isn’t a “which framework wins” article dressed up as a tutorial. Each stack has a legitimate reason to exist in 2026: React remains the default recommendation for most startups because of its hiring pool and library ecosystem, Vue keeps winning teams that value a shorter ramp-up time, and Angular keeps its grip on regulated and enterprise codebases that need dependency injection and a batteries-included testing setup out of the box. Building the same feature three times is the fastest way to feel those tradeoffs instead of just reading about them.

Google · Preferred Sources

Don't miss new tech stories on Google

Add Tech Insider once in the Google app and our stories appear in your news suggestions.

Add Now

What You’ll Build in This Tutorial

Each of the three projects implements the same small feature set: a todo list that loads tasks from the free JSONPlaceholder API, shows a loading state while the request is in flight, lets you add a new local task, lets you mark a task complete, and shows an error message if the fetch fails. Keeping the feature set identical is the point — it’s the only way to compare setup friction, code volume, and runtime behavior fairly instead of comparing a toy React app against a fully-featured Angular one.

The scope is deliberately small. A todo list touches the four things that matter most when you’re evaluating a framework hands-on: component state, list rendering, event handling, and an asynchronous network call. It doesn’t touch routing, authentication, or a database, which is exactly why it’s a fair basis for comparison — a bigger app would introduce library choices (which router, which state manager) that vary independently of the framework itself and would muddy the comparison.

You’ll end up with a folder structure like this on disk:

framework-comparison/
  react-todo/     (Vite + React 19.2 + TypeScript)
  vue-todo/       (create-vue + Vue 3.5 + TypeScript)
  angular-todo/   (Angular CLI 22 + standalone components)

Each project gets its own dev server, its own test suite, and its own production build, so you can run `npm run build` in all three and compare the output directly. If you only care about one comparison — say, react vs vue specifically — you can skip the Angular sections and still get a complete, working project.

Prerequisites: Exact Versions You Need in August 2026

Framework tutorials go stale fast because nobody pins the exact versions they tested against. Here’s exactly what this walkthrough uses, verified against each project’s official release notes as of mid-August 2026. Run `npm view <package> version` before you start if you want to confirm you’re on the same numbers.

ToolVersion Used HereNotes
Node.js22.x LTSRequired by Angular 22’s compatibility matrix
React19.2.8 (Jul 21, 2026)Latest patch on the 19.2 line
Vite8.2.1 (Aug 6, 2026)Build tool for both React and Vue projects
Next.js16.3.0 (Aug 3, 2026)Used in the SSR step
Vue3.5.41 (Aug 5, 2026)Latest stable on the 3.5 line
create-vuelatestOfficial Vite-based scaffolder; Vue CLI is in maintenance mode
Nuxt4.5.2 (Aug 5, 2026)Used in the SSR step; Nuxt 3 is now end-of-life
Angular22.1.1 (Aug 7, 2026)v22 shipped Jun 3, 2026 with stable Signal Forms
TypeScript6.0.xMinimum required by Angular 22’s official compatibility table
Vitestlatest 4.xDefault test runner for all three projects in this guide

You’ll also need a code editor with TypeScript support and about 2GB of free disk space once all three `node_modules` folders exist side by side. If you’ve read our React vs Vue build tutorial before, the React and Vue steps here will feel familiar, but the versions, the SSR comparison, and the Angular build are new.

Understanding the Three Reactivity Models Before You Start

Before writing any code, it’s worth understanding what actually differs under the hood, because it explains almost every syntax decision you’ll hit in the steps below. React re-renders a component function from top to bottom whenever its state changes, and relies on a virtual DOM diff to figure out what actually needs to update in the browser. That’s why React code reads like a pure function of its state — call `setTodos` with a new array, and the whole component body runs again to compute the next UI.

Vue takes a different approach: `ref()` and `reactive()` wrap your data in JavaScript Proxies that track exactly which parts of the template read which piece of state. When `todo.completed` changes, only the DOM nodes that actually depend on `todo.completed` update — the rest of the component function doesn’t re-run at all. That’s why Vue lets you mutate objects and arrays directly, something that silently breaks in React.

Angular 22’s signals work more like Vue’s proxies than React’s virtual DOM: a `signal()` tracks every place it’s read, and only those specific bindings recompute when `.set()` or `.update()` is called. The difference from Vue is syntactic more than architectural — you call a signal like a function to read it (`todos()`), where Vue lets you read `.value` on a ref, or nothing at all inside a template. Knowing this going in makes the component code in Steps 3, 6, and 9 much easier to read, since the same underlying problem (re-render efficiently when data changes) gets three genuinely different solutions.

Step 1 – Install Node.js and Set Up Your Package Manager

Angular 22’s compatibility matrix requires Node.js 22.22.3 or newer, or the Node 24/26 lines — older Node 20 installs will fail the `ng new` preflight check. Install Node 22 LTS from nodejs.org or via a version manager like nvm, then confirm the toolchain before creating any project.

node -v
# should print v22.x.x or newer

npm -v
# should print 10.x or newer

mkdir framework-comparison && cd framework-comparison

You can use npm throughout this tutorial, but pnpm will save real time and disk space once you have three separate `node_modules` trees. Enable it with Node’s built-in corepack if you want to follow along with pnpm commands: `corepack enable pnpm`. Everything below shows npm commands since that’s the default every scaffolder assumes.

Step 2 – Scaffold the React 19.2 Project with Vite

React itself ships no CLI or build tool — the officially recommended path in 2026 is Vite’s React-TypeScript template. Run the scaffolder inside your `framework-comparison` folder:

npm create vite@latest react-todo -- --template react-ts
cd react-todo
npm install
npm run dev

Vite’s dev server should be live at `http://localhost:5173` in under two seconds — that near-instant startup, powered by native ES modules and Vite 8’s Rolldown-based bundler, is one of the biggest quality-of-life differences from older React tooling like Create React App, which is no longer maintained. Confirm `react` resolves to 19.2.x with `npm ls react`, then move on to building the component.

Step 3 – Build the React Todo Component with Hooks

Replace the contents of `src/App.tsx` with a component that holds local state for the todo list and a text input. React’s model is explicit: state lives in `useState`, and every state change triggers a re-render of the component tree below it.

import { useState } from 'react';
import { useTodos } from './useTodos';

type Todo = { id: number; title: string; completed: boolean };

export default function App() {
  const { todos, loading, error, setTodos } = useTodos();
  const [draft, setDraft] = useState('');

  function addTodo() {
    if (!draft.trim()) return;
    setTodos((prev: Todo[]) => [
      { id: Date.now(), title: draft, completed: false },
      ...prev,
    ]);
    setDraft('');
  }

  function toggleTodo(id: number) {
    setTodos((prev: Todo[]) =>
      prev.map((t) => (t.id === id ? { ...t, completed: !t.completed } : t))
    );
  }

  if (loading) return <p>Loading tasks…</p>;
  if (error) return <p>Could not load tasks: {error}</p>;

  return (
    <main>
      <h1>React Todo</h1>
      <input value={draft} onChange={(e) => setDraft(e.target.value)} />
      <button onClick={addTodo}>Add</button>
      <ul>
        {todos.map((t) => (
          <li key={t.id} onClick={() => toggleTodo(t.id)}>
            {t.completed ? '✓ ' : ''}
            {t.title}
          </li>
        ))}
      </ul>
    </main>
  );
}

Notice how much of this file is JSX markup interleaved with logic — that’s React’s core design decision, and it’s the single biggest adjustment for developers coming from Vue or Angular’s template syntax. There’s no separate template file; the render output and the component logic live in the same function.

Step 4 – Fetch Data in React with a Custom Hook

Create `src/useTodos.ts` to isolate the data-fetching logic from the component. This is the idiomatic React pattern for reusable stateful logic — a custom hook that wraps `useState` and `useEffect`.

import { useEffect, useState } from 'react';

type Todo = { id: number; title: string; completed: boolean };

export function useTodos() {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    fetch('https://jsonplaceholder.typicode.com/todos?_limit=10')
      .then((res) => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res.json();
      })
      .then((data: Todo[]) => setTodos(data))
      .catch((err) => setError(err.message))
      .finally(() => setLoading(false));
  }, []);

  return { todos, loading, error, setTodos };
}

The empty dependency array `[]` tells React to run this effect exactly once, on mount. Forgetting that array — or filling it with the wrong values — is the single most common React bug beginners hit, and it’s worth internalizing before moving to Vue’s very different reactivity model in the next step.

Step 5 – Scaffold the Vue 3.5 Project with create-vue

Vue CLI is officially in maintenance mode and no longer recommended for new projects — the Vue release notes and migration guide both point new projects at `create-vue`, which scaffolds a Vite-powered, TypeScript-ready project by default. From your `framework-comparison` folder:

npm create vue@latest vue-todo
# Select: TypeScript = Yes, everything else = No for this tutorial
cd vue-todo
npm install
npm run dev

Because create-vue and the React Vite template share the same underlying Vite 8.2 build tool, dev-server startup time is nearly identical between the two — the real differences show up in the component syntax, not the tooling speed. The dev server runs at `http://localhost:5173` by default, same as the React project, so make sure you’re not running both at once without changing a port.

Step 6 – Build the Vue Todo Component with the Composition API

Replace `src/App.vue` with a single-file component using `<script setup>`, the standard syntax for the Composition API since Vue 3. Note how the template is a separate block from the script — the opposite structural choice from React’s JSX.

<script setup lang="ts">
import { ref } from 'vue';
import { useTodos } from './useTodos';

const { todos, loading, error } = useTodos();
const draft = ref('');

function addTodo() {
  if (!draft.value.trim()) return;
  todos.value.unshift({ id: Date.now(), title: draft.value, completed: false });
  draft.value = '';
}

function toggleTodo(id: number) {
  const todo = todos.value.find((t) => t.id === id);
  if (todo) todo.completed = !todo.completed;
}
</script>

<template>
  <main>
    <h1>Vue Todo</h1>
    <p v-if="loading">Loading tasks…</p>
    <p v-else-if="error">Could not load tasks: {{ error }}</p>
    <template v-else>
      <input v-model="draft" />
      <button @click="addTodo">Add</button>
      <ul>
        <li v-for="t in todos" :key="t.id" @click="toggleTodo(t.id)">
          {{ t.completed ? '✓ ' : '' }}{{ t.title }}
        </li>
      </ul>
    </template>
  </main>
</template>

The `v-model` directive on the input replaces React’s manual `value` plus `onChange` pairing with two-way binding in a single attribute. Mutating `todo.completed` directly and having the UI update automatically, with no `setTodos` spread copy required, is Vue’s proxy-based reactivity system doing the work that React’s immutable state updates require you to do by hand.

Step 7 – Fetch Data in Vue 3 with Composition API and Fetch

Create `src/useTodos.ts` as a composable — Vue’s equivalent of a React hook, built around `ref` instead of `useState`.

import { ref, onMounted } from 'vue';

type Todo = { id: number; title: string; completed: boolean };

export function useTodos() {
  const todos = ref<Todo[]>([]);
  const loading = ref(true);
  const error = ref<string | null>(null);

  onMounted(async () => {
    try {
      const res = await fetch('https://jsonplaceholder.typicode.com/todos?_limit=10');
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      todos.value = await res.json();
    } catch (err) {
      error.value = (err as Error).message;
    } finally {
      loading.value = false;
    }
  });

  return { todos, loading, error };
}

`onMounted` is the Vue equivalent of React’s `useEffect(() => {}, [])` — both run once when the component enters the DOM, but Vue’s version reads as a plain lifecycle hook rather than a dependency-array trick, which is one reason developers new to front-end frameworks often find Vue’s data-fetching code easier to reason about on first read.

Step 8 – Scaffold the Angular 22 Project with Standalone Components

Angular ships its own CLI, unlike React and Vue, and it does considerably more work upfront — routing, HTTP client wiring, and testing setup are all generated for you. Install the CLI and scaffold a new project:

npm install -g @angular/cli@22
ng new angular-todo --standalone --style=css --routing=false
cd angular-todo
ng serve

The `–standalone` flag matters: Angular has been moving away from `NgModule`-based apps since Angular 17, and by Angular 22 standalone components are the default project shape recommended in the official docs. `ng serve` takes noticeably longer to cold-start than the Vite dev servers in the React and Vue projects — that’s the tradeoff for Angular’s more batteries-included build pipeline.

Step 9 – Build the Angular Todo Component with Signals

Angular 22 stabilized Signal Forms and leans hard into signals as the primary reactivity primitive, replacing a lot of what used to require RxJS observables for simple local state. Edit `src/app/app.component.ts`:

import { Component, signal } from '@angular/core';
import { TodosService, Todo } from './todos.service';

@Component({
  selector: 'app-root',
  standalone: true,
  template: `
    <main>
      <h1>Angular Todo</h1>
      <p *ngIf="loading()">Loading tasks…</p>
      <p *ngIf="error()">Could not load tasks: {{ error() }}</p>
      <div *ngIf="!loading() && !error()">
        <input [(ngModel)]="draft" />
        <button (click)="addTodo()">Add</button>
        <ul>
          <li *ngFor="let t of todos()" (click)="toggleTodo(t.id)">
            {{ t.completed ? '✓ ' : '' }}{{ t.title }}
          </li>
        </ul>
      </div>
    </main>
  `,
})
export class AppComponent {
  draft = '';
  todos;
  loading;
  error;

  constructor(private todosService: TodosService) {
    this.todos = this.todosService.todos;
    this.loading = this.todosService.loading;
    this.error = this.todosService.error;
  }

  addTodo() {
    if (!this.draft.trim()) return;
    this.todosService.add(this.draft);
    this.draft = '';
  }

  toggleTodo(id: number) {
    this.todosService.toggle(id);
  }
}

Calling a signal like a function — `todos()` instead of `todos` — is the syntactic tell that distinguishes Angular 22 code from older Angular. It’s a deliberate design choice that lets Angular’s change detector track exactly which signals a template reads, closer to how React and Vue already track dependencies, without RxJS’s steeper learning curve.

Step 10 – Fetch Data in Angular with HttpClient and Signals

Generate a service to hold the data-fetching logic, keeping it out of the component the same way the React hook and Vue composable did:

ng generate service todos
import { Injectable, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';

export type Todo = { id: number; title: string; completed: boolean };

@Injectable({ providedIn: 'root' })
export class TodosService {
  todos = signal<Todo[]>([]);
  loading = signal(true);
  error = signal<string | null>(null);

  constructor(private http: HttpClient) {
    this.http
      .get<Todo[]>('https://jsonplaceholder.typicode.com/todos?_limit=10')
      .subscribe({
        next: (data) => this.todos.set(data),
        error: (err) => this.error.set(err.message),
        complete: () => this.loading.set(false),
      });
  }

  add(title: string) {
    this.todos.update((list) => [{ id: Date.now(), title, completed: false }, ...list]);
  }

  toggle(id: number) {
    this.todos.update((list) =>
      list.map((t) => (t.id === id ? { ...t, completed: !t.completed } : t))
    );
  }
}

You’ll need to add `provideHttpClient()` and `FormsModule` to `app.config.ts` and the component’s imports for `HttpClient` and `ngModel` to resolve — Angular’s explicit provider registration is more ceremony than React or Vue require for a fetch call, but it buys you dependency injection and testability that neither of the other two frameworks builds in by default.

Step 11 – Add Tests to All Three Projects with Vitest

Testing tooling has converged hard around Vitest in 2026. React projects pair it with React Testing Library, Vue projects pair it with Vue Test Utils, and Angular made Vitest its default test runner starting with Angular 21, officially replacing Karma and Jasmine, which had been deprecated since 2023. Install the React testing stack:

npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import App from './App';

describe('React Todo', () => {
  it('renders the heading', () => {
    render(<App />);
    expect(screen.getByText('React Todo')).toBeDefined();
  });
});

The Vue and Angular equivalents follow the same pattern — `@vue/test-utils` mounts the SFC and asserts on rendered text, while Angular’s CLI-generated `.spec.ts` files now run on Vitest out of the box in new Angular 22 projects, so `ng test` works with zero extra configuration. If you’re comparing testing setups specifically, our Vitest vs Jest breakdown covers the runner-level tradeoffs in more depth.

Step 12 – Compare Bundle Size, Build Time, and Dev Server Speed

Run `npm run build` in each of the three project folders and inspect the output directory. The numbers below are approximate, gathered from current 2026 cross-framework bundle-size benchmarks and each framework’s own runtime measurements, not a single hard number you should treat as exact for your own app — actual size depends heavily on what you import.

MetricReact 19.2 + ViteVue 3.5 + ViteAngular 22
Core runtime, gzipped~50–60 KB~15–20 KB~60–70 KB
Core runtime, minified~160–190 KB~60–65 KB~195 KB
Cold dev server startNear-instant (Vite 8)Near-instant (Vite 8)Several seconds (webpack/esbuild pipeline)
CLI-generated testsManual setup requiredManual setup requiredIncluded by default

Vue’s smaller core runtime is a direct result of its more granular internals and lack of a built-in HTTP client or router in the base package — both React and Angular pull in more by default. None of these differences are large enough to matter for a todo app; they start to matter once you’re shipping dozens of routes and want to keep initial load time under control on slow connections.

Here’s what a successful production build looks like in the terminal for each stack, so you can confirm your own output matches before moving on:

# React (Vite) production build
vite v8.2.1 building for production...
✓ 34 modules transformed.
dist/index.html                  0.46 kB
dist/assets/index-C8f2a9k1.css    1.21 kB
dist/assets/index-B7x0m4qP.js    62.87 kB
✓ built in 612ms

# Vue (create-vue) production build
vite v8.2.1 building for production...
✓ 28 modules transformed.
dist/index.html                  0.41 kB
dist/assets/index-D4n1p8sT.js    41.09 kB
✓ built in 548ms

# Angular production build
Application bundle generation complete. [4.912 seconds]
Initial chunk files | Names         |  Raw size
main-XJ4KQK2P.js    | main          | 187.40 kB
styles-4VLBJPQR.css | styles        |   0.62 kB
Output location: dist/angular-todo

Step 13 – Add Server-Side Rendering with Next.js 16.3 and Nuxt 4.5

None of the three plain client-side projects above render on the server, which hurts first-paint time and SEO for content-heavy pages. React’s ecosystem answer is Next.js, currently at version 16.3.0 (released August 3, 2026); Vue’s is Nuxt, at 4.5.2 (released August 5, 2026, with the Nuxt 3 line now officially end-of-life). Both scaffold in one command:

# React + SSR
npx create-next-app@latest react-todo-ssr --typescript

# Vue + SSR
npx nuxi@latest init vue-todo-ssr

Both meta-frameworks now run on Vite 8.2 under the hood, which is why their dev-server behavior feels similar despite the very different component syntax. Angular’s SSR story is different — it’s built into the core framework via `@angular/ssr` rather than living in a separate meta-framework, so `ng add @angular/ssr` adds server rendering directly to the Angular project from Step 8 without switching tools. Check the Next.js release blog and Nuxt blog before you ship either in production, since both frameworks shipped security patches in July 2026 that you’ll want to be on top of.

Step 14 – Deploy All Three Apps to Production

All three projects build to static assets (or a Node server for the SSR variants) that deploy the same way to any static host or edge platform. For the plain Vite builds from Steps 2–10, push each folder to its own Git repo and connect it to a platform like Vercel, Netlify, or Cloudflare Pages — all three auto-detect Vite, Next.js, Nuxt, and Angular CLI projects without extra config.

# Angular production build
ng build --configuration production

# React / Vue production build (same command for both)
npm run build

Angular’s production build runs additional optimization passes — ahead-of-time compilation and tree-shaking of unused Angular modules — that take noticeably longer than Vite’s Rolldown-based bundling for the React and Vue projects. That extra build time is a one-time CI cost, not something end users notice, so don’t let it sway a framework choice on its own.

Routing and Multi-Page Navigation: React Router vs Vue Router vs Angular Router

The todo app in this tutorial is deliberately a single page, but almost nothing you ship stays that way. React doesn’t include a router at all — React Router or, if you adopted Next.js in Step 13, its file-based app router are both separate installs on top of the base library. Vue is similar: Vue Router is an official, first-party package, but it’s a separate `npm install vue-router` away from a fresh create-vue project unless you opt into it during scaffolding.

Angular is the outlier again. Routing ships as part of the framework itself, configured through the same CLI that generated your project — recall the `–routing=false` flag used in Step 8 to skip it for this tutorial. Turn it back on with `ng generate module app-routing` on an existing project, or answer “yes” to the CLI’s routing prompt on a new one, and you get a router that’s already wired into Angular’s dependency injection system, with route guards and resolvers following the same patterns as the rest of the framework. That consistency is a real advantage on large teams, at the cost of learning Angular’s specific routing API instead of a more widely-used third-party one.

State Management at Scale: Pinia, Zustand, and Angular Signal Stores

Local component state, as used throughout this tutorial, works fine until two unrelated components need to share the same data — at that point you need a state management layer, and each ecosystem has a clear default in 2026. React’s community has mostly settled on Zustand for new projects, a small hook-based store that avoids Redux’s boilerplate while still centralizing state outside the component tree; Redux Toolkit remains common in older, larger codebases that already invested in the Redux pattern.

Vue’s answer is Pinia, which replaced Vuex as the officially recommended store years ago and integrates directly with the Composition API — a Pinia store looks almost identical to the `useTodos` composable built in Step 7, just registered globally instead of scoped to one component. Angular, true to form, increasingly handles this with plain injectable services built on signals, exactly like the `TodosService` from Step 10 — because Angular’s dependency injection already gives every component access to the same service instance, a lot of Angular apps in 2026 don’t reach for a dedicated state library like NgRx unless the state graph gets genuinely complex, with time-travel debugging or undo/redo requirements.

React vs Vue vs Angular in August 2026: Which One to Pick

Adoption data hasn’t shifted the underlying order in years, even as all three frameworks keep shipping major releases. The Stack Overflow 2025 Developer Survey puts React at 44.7% usage among professional developers, ahead of Angular at 18.2% and Vue at 17.6%. GitHub activity and npm download volume tell a similar story about ecosystem size, though not about day-to-day developer experience.

SignalReactVueAngular
Stack Overflow 2025 usage44.7%17.6%18.2%
npm weekly downloads (core package)~130M~10–12M~4–5M (@angular/core)
GitHub stars (core repo)~244k~53–54k~100–101k
Default test runner (2026)Vitest / JestVitestVitest (default since v21)
Official meta-frameworkNext.js 16.3Nuxt 4.5Built-in @angular/ssr

React’s raw ecosystem size makes it the safest default for hiring and third-party library availability. Vue’s smaller runtime and gentler learning curve keep it the fastest to onboard a team onto. Angular’s built-in dependency injection, testing, and SSR make it the framework that needs the fewest extra architectural decisions on a large, long-lived enterprise codebase — which is exactly why it holds steady in large organizations even without React’s raw popularity numbers. If your team already leans one way, our dedicated Angular vs React comparison and React-to-Vue migration guide go deeper on those specific pairs.

Accessibility: What’s New Across All Three in 2026

Accessibility used to be the area where all three frameworks left the most work to the developer — none of them shipped accessible components out of the box, so ARIA attributes, focus management, and keyboard navigation were entirely manual work layered on top of whichever framework you picked. That changed meaningfully with Angular 22, which stabilized Angular ARIA, a set of accessible primitive components (comboboxes, listboxes, tree views) that handle keyboard interaction and screen-reader semantics correctly by default, alongside the Signal Forms work covered earlier in this tutorial.

React and Vue don’t have an equivalent first-party primitive library as of August 2026 — both ecosystems lean on third-party libraries like Radix UI or Headless UI for React, and Reka UI (formerly Radix Vue) for Vue, to get the same accessible-by-default behavior. That’s not necessarily a disadvantage; those libraries are mature and widely used. But if your team is building an internal design system from scratch and wants accessible primitives without adding a third-party dependency, Angular’s built-in option is currently unique among the three frameworks covered in this tutorial.

Regardless of which framework you pick, the todo app built in this tutorial has real accessibility gaps worth fixing before shipping anything real: the plain `<input>` elements need associated `<label>` tags, the clickable `<li>` elements for toggling a todo should be real `<button>` elements or carry `role=”button”` and keyboard handlers, and the loading and error states should be wrapped in an `aria-live` region so screen readers announce the state change automatically instead of silently updating the DOM.

Common Pitfalls When Comparing React, Vue, and Angular

  • Comparing an unoptimized Angular build against production React/Vue builds. Angular’s dev build is noticeably larger than its production build; always compare `ng build –configuration production` output, not the dev server payload.
  • Mutating React state directly, the way you would in Vue. `todos.push(…)` on a React state array won’t trigger a re-render because React compares object references, not contents — always create a new array or object.
  • Forgetting `provideHttpClient()` in Angular’s standalone bootstrap. Without it, `HttpClient` injection fails silently at runtime with a cryptic `NullInjectorError`, not a build-time error.
  • Judging bundle size from a single component instead of a realistic route. Framework runtime overhead gets diluted as an app grows; a five-line comparison favors whichever framework has the smallest base runtime, which isn’t always the deciding factor at scale.
  • Assuming Vue CLI is still the recommended tool. Tutorials written before 2023 still reference `vue-cli`, which is now in maintenance mode — use `create-vue` for any new project.
  • Treating npm download counts as a satisfaction metric. React’s ~130M weekly downloads reflect ecosystem breadth (every dependency of a dependency counts), not that developers prefer it 13-to-1 over Vue in day-to-day use.

Troubleshooting: 8 Common Setup and Runtime Errors

  • “ng: command not found” after installing Angular CLI. The global npm bin directory usually isn’t on your PATH by default on macOS/Linux; run `npm config get prefix` and add `<prefix>/bin` to your shell profile.
  • Vite dev server starts but shows a blank white screen. Almost always a JavaScript error thrown before the first render; open the browser console, not the terminal, since Vite won’t surface runtime errors in the CLI output.
  • React: “Too many re-renders” error. You’re calling a state setter directly in the render body instead of inside an event handler or effect — wrap it in a function reference, like `onClick={addTodo}` rather than `onClick={addTodo()}`.
  • Vue: template changes don’t seem to reflect after saving. Vite’s hot module replacement occasionally misses `<script setup>` edits inside deeply nested composables; a hard browser refresh usually resolves it faster than restarting the dev server.
  • Angular: “NG0203: signal() can only be used within an injection context.” You declared a signal outside a component or injectable class field — move it inside the class body, not a standalone function.
  • CORS errors fetching from JSONPlaceholder or any external API. JSONPlaceholder allows cross-origin requests by default; if you swap in your own API, add the appropriate `Access-Control-Allow-Origin` header server-side rather than trying to work around it client-side.
  • TypeScript errors about implicit `any` on fetch responses. `res.json()` returns `Promise<any>` by design; always cast or type-annotate the destructured result, as shown in the hooks and composables above.
  • `ng build` fails with a Node engine mismatch warning. Angular 22 requires Node 22.22.3+, 24.15.0+, or 26.0.0+ specifically — intermediate Node versions between those ranges will fail the CLI’s engine check even if they’re technically newer than 20.

Advanced Tips for Production-Grade Apps

Once the basic todo app works in all three frameworks, a few practices separate a tutorial project from something you’d actually ship. First, add a proper state management layer once your app outgrows local component state — Zustand or Redux Toolkit for React, Pinia for Vue, and Angular’s own signal-based stores or NgRx for Angular all solve the same “shared state across distant components” problem with different levels of ceremony.

Second, don’t skip error boundaries and loading skeletons in production. The `if (loading)` / `if (error)` pattern used in this tutorial is fine for a demo, but a real app should use React’s `<Suspense>` and error boundary components, Vue’s `<Suspense>` built-in, and Angular’s `@defer` blocks (stable since Angular 17) to avoid layout shift while data loads.

Third, lint and format consistently across all three codebases if your team maintains more than one. A fast, framework-agnostic linter matters more once you’re running it in CI across three separate repos — see our ESLint vs Biome vs Oxlint comparison if build-time linting speed is slowing down your pipeline. And if you’re picking an editor to work across React, Vue, and Angular code side by side, our Cursor vs Windsurf vs Zed roundup covers which AI-assisted editors handle multi-framework projects best.

Finally, budget real time for the SSR step if you plan to ship any of these to production. Both Next.js 16.3 and Nuxt 4.5 patched high-severity CVEs in their July 2026 security releases — check each project’s changelog and pin to the patched versions (Next.js 16.2.11+/15.5.21+, Nuxt 4.5.1+/3.21.10+) before deploying anything public-facing.

Frequently Asked Questions

Is React still more popular than Vue and Angular in 2026?

Yes. The Stack Overflow 2025 Developer Survey puts React usage at 44.7% among professional developers, well ahead of Angular’s 18.2% and Vue’s 17.6%, and that ordering has held steady for several years.

Which framework has the smallest bundle size?

Vue 3.5’s core runtime is the smallest of the three, at roughly 15–20 KB gzipped, compared to React’s 50–60 KB and Angular’s 60–70 KB. The gap shrinks as an app grows and pulls in more shared dependencies like routing and state management.

Do I need Next.js or Nuxt, or can I ship plain React or Vue?

Plain client-rendered React or Vue apps (what you build in Steps 2–7) are fine for internal tools and authenticated dashboards where SEO doesn’t matter. For public, content-heavy pages, Next.js or Nuxt’s server rendering meaningfully improves first paint and search indexing.

Is Vue CLI dead?

It’s in maintenance mode, not deleted — existing projects still build, but the Vue team officially recommends `create-vue` and Vite for all new projects, and that’s what this tutorial uses.

What replaced Karma for Angular testing?

Vitest. Karma and Jasmine were deprecated in 2023, and starting with Angular 21, Vitest became the default test runner generated by the Angular CLI, with support for Jest and the Web Test Runner being removed as of Angular 22.

Can I use signals in React the way Angular does?

Not natively. React’s reactivity model is still built on `useState` and re-renders rather than fine-grained signals. Angular and, to a lesser extent, Vue’s `ref()` both use signal-style reactivity that only re-computes what actually changed, which is a genuine architectural difference, not just a syntax preference.

Which framework is easiest for a beginner to learn first?

Vue’s single-file component structure and built-in two-way binding tend to have the shortest path from zero to a working app, since JSX and Angular’s dependency injection both carry a steeper initial learning curve. That said, React’s ecosystem size means far more beginner tutorials and Stack Overflow answers exist for it specifically.

Is it realistic to migrate an existing React app to Vue or Angular?

It’s a significant rewrite, not a port — component logic, state management, and routing all need to be re-architected around the target framework’s model. See our dedicated React to Vue 3 migration guide for a step-by-step approach if you’re planning one.

Does Angular’s steeper learning curve still hold up in 2026?

Somewhat less than it used to. Standalone components removed the need to understand `NgModule` wiring before writing your first component, and signals replace a lot of what previously required learning RxJS operators just to fetch and display data. Dependency injection and the CLI’s opinionated project structure are still a bigger initial investment than React or Vue, but the gap has narrowed compared to Angular before version 17.

Which of the three has the best official documentation?

All three now maintain interactive, example-driven docs sites — Vite’s own documentation plus the framework-specific sites at react.dev, vuejs.org, and angular.dev all include runnable code playgrounds. Vue’s guide is generally considered the most linear and beginner-friendly to read start to finish; Angular’s is the most exhaustive for advanced topics like change detection internals.

Related Coverage

Elias Virtanen

Elias Virtanen

Cybersecurity Analyst

Elias Virtanen is the Cybersecurity Analyst at Tech Insider, bringing hands-on expertise from his background in penetration testing and security consulting. He previously worked as a security researcher at F-Secure in Helsinki, where he focused on threat intelligence and vulnerability disclosure. Elias covers ransomware trends, zero-trust architecture, and the evolving regulatory landscape including NIS2 and the EU Cyber Resilience Act. He holds a CISSP certification and an MSc in Information Security from Aalto University.

View all articles