This is by no means intended to be an in-depth guide to the differences between React and Angular. On the contrary, it is a first-glance perspective from a developer who has been working with React and has historically avoided Angular’s OOP boilerplate, but finally decided to take a look at how things are going on the other side of the fence.
Starting with my biggest pain point: boilerplate. I quickly discovered that just looking at Angular code snippets does not do justice to the work its CLI puts into handling that complexity. It defines naming conventions, folder structures, creates files, and manages required imports. Part of what can be considered boilerplate is also related to project structure, and becoming more familiar with MVC helps in dealing with the mental model of OOP-based programming languages. This makes Angular feel well structured and predictable, which is probably why it is often deemed an enterprise framework suited for large organizations.
Over the last few years, Angular has also been shifting toward a more approachable development experience, with its relatively recent move to standalone components as the default, and toward a much more performant framework with the adoption of a signal-based architecture, a path that the React team unfortunately has explicitly chosen not to pursue.
At the end of the day, both React and Angular will remain, at least for a while, the two most popular front-end frameworks, so it is only wise to be familiar with both.
Key Philosophical Differences
| Aspect | React | Angular |
|---|---|---|
| Language | JavaScript or TypeScript | TypeScript |
| Paradigm | Functional, UI/State/Logic coupled | Object-oriented, MVC-like |
| Templates | JSX (JavaScript Syntax Extension) | HTML templates + directives |
| State | Immutable updates (useState) | Mutable class properties |
| Data Flow | Unidirectional (parent to child) | Two-way binding |
| Rendering | DOM + Virtual DOM (diff algorithm) | DOM with change detection (Zone.js or signals) |
| Philosophy | Minimal, "library" | Complete framework |
TL;DR
| React Concept | Angular Equivalent |
|---|---|
useState |
Component properties / signals |
| Context API | @Injectable Service (singleton via DI) |
useEffect |
Lifecycle hooks (ngOnInit, ngOnDestroy) / effect()
|
| Props / parent → child data |
@Input() / signal inputs |
Conditional rendering (&&, ?:) |
*ngIf / @if
|
Lists / keys (.map + key) |
*ngFor + trackBy / @for
|
| Inline style objects |
[style] / [ngStyle] / computed signal objects |
Two-way binding (value + onChange) |
[(ngModel)] / reactive forms |
State Management
One of the key aspects of modern frameworks is how they handle state and changes to that state in order to display dynamic, up-to-date content.
In React, state updates are expected to be immutable so React can detect changes via reference comparison. Updating state variables directly does not trigger re-renders, so state is managed through hooks such as useState.
In Angular, component properties can be mutated directly. Change detection is handled by the framework, or, when using signals, by precise reactive updates.
React - useState
function Counter() {
const [count, setCount] = useState(0);
const increment = () => setCount((val) => val + 1);
const decrement = () => setCount((val) => val - 1);
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
}
Angular - Class Properties
export class CounterComponent {
count = 0;
increment() {
this.count++;
}
decrement() {
this.count--;
}
}
Signals (Angular 16+)
export class CounterComponent {
count = signal(0);
increment() {
this.count.update((val) => val + 1);
}
decrement() {
this.count.update((val) => val - 1);
}
}
<div>
<p>Count: {{ count() }}</p>
<button (click)="increment()">+</button>
<button (click)="decrement()">-</button>
</div>
Global / Shared State
Another key aspect of modern frameworks is how state that must be shared across multiple, unrelated components is handled.
In React, global state is commonly managed via the Context API, often combined with useReducer or external libraries that help optimize and structure state management.
In Angular, shared state is typically handled via services, which are singletons by default and can be injected where needed, or via signals that act as reactive global stores.
React – Context API
import { createContext, useContext, useState } from "react";
const ThemeContext = createContext<"light" | "dark">("light");
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<"light" | "dark">("light");
return (
<ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider>
);
}
function App() {
return (
<ThemeProvider value="light">
<MyComponent />
</ThemeProvider>
);
}
function MyComponent() {
const theme = useContext(ThemeContext);
return <div>Current theme: {theme}</div>;
}
Angular – Services (Singleton State)
import { Injectable } from "@angular/core";
@Injectable({ providedIn: "root" })
export class ThemeService {
theme: "light" | "dark" = "light";
}
import { Component } from "@angular/core";
import { ThemeService } from "./theme.service";
@Component({
selector: "app-my-component",
template: `<div>Current theme: {{ themeService.theme }}</div>`,
})
export class MyComponent {
constructor(public themeService: ThemeService) {}
}
Signals (Angular 16+)
import { Injectable, signal } from "@angular/core";
@Injectable({ providedIn: "root" })
export class ThemeService {
theme = signal<"light" | "dark">("light");
toggle() {
this.theme.update((t) => (t === "light" ? "dark" : "light"));
}
}
@Component({
selector: "app-my-component",
template: `<div>Current theme: {{ themeService.theme() }}</div>`,
})
export class MyComponent {
constructor(public themeService: ThemeService) {}
}
Class Binding
Let's assume btn is a class statically applied to the button while active and primary are dynamically applied.
React - String Manipulation
function MyButton() {
const [isActive, setIsActive] = useState(true);
const [isPrimary, setIsPrimary] = useState(false);
return (
<button
className={`btn ${isActive ? "active" : ""} ${
isPrimary ? "primary" : ""
}`}
>
Click
</button>
);
}
Angular - Boolean Binding
export class MyButtonComponent {
isActive = true;
isPrimary = false;
}
<button class="btn" [class.active]="isActive" [class.primary]="isPrimary">
Click
</button>
Or with ngClass for multiple classes:
export class MyButtonComponent {
isActive = true;
isPrimary = false;
get btnClasses() {
return {
btn: true,
active: this.isActive,
primary: this.isPrimary,
};
}
}
Signals (Angular 16+)
export class MyButtonComponent {
isActive = signal(true);
isPrimary = signal(false);
btnClasses = computed(() => ({
btn: true,
active: this.isActive(),
primary: this.isPrimary(),
}));
}
<button [ngClass]="btnClasses">Click</button>
Style Binding
fontSize is a static value while width and backgroundColor are dynamic values.
React - Style Objects
function MyButton() {
const [width, setWidth] = useState(200);
const [isActive, setIsActive] = useState(true);
const buttonStyles = {
fontSize: "16px",
width: `${width}px`,
backgroundColor: isActive ? "green" : "teal",
};
return <button style={buttonStyles}>Click</button>;
}
Angular - Property Binding
Individual style properties:
export class MyButtonComponent {
width = 200;
isActive = true;
}
<button
style="font-size: 16px;"
[style.width.px]="width"
[style.background-color]="isActive ? 'green' : 'teal'"
>
Click
</button>
Or with ngStyle for multiple styles:
export class MyButtonComponent {
width = 200;
isActive = true;
get buttonStyles(): Partial<CSSStyleDeclaration> {
return {
fontSize: "16px",
width: this.width + "px",
backgroundColor: this.isActive ? "green" : "teal",
};
}
}
Signals (Angular 16+)
export class MyButtonComponent {
width = signal(200);
isActive = signal(true);
buttonStyles = computed<Partial<CSSStyleDeclaration>>(() => ({
fontSize: "16px",
width: this.width() + "px",
backgroundColor: this.isActive() ? "green" : "teal",
}));
}
<button [ngStyle]="buttonStyles">Click</button>
Conditional Rendering
React - Logical Operators (AND and Ternary)
function MyComponent() {
const [isVisible, setIsVisible] = useState(true);
const [user, setUser] = useState({ name: "John" });
return (
<div>
{isVisible && <p>This is visible</p>}
{user ? <p>Hello, {user.name}</p> : <p>Please log in</p>}
</div>
);
}
Angular - Structural Directives (Angular 16-)
export class MyComponent {
isVisible = true;
user = { name: "John" };
}
<div>
<p *ngIf="isVisible">This is visible</p>
<p *ngIf="user; else noUser">Hello, {{ user.name }}</p>
<ng-template #noUser>
<p>Please log in</p>
</ng-template>
</div>
Angular - Control Flow (Angular 17+)
<div>
@if (isVisible) {
<p>This is visible</p>
} @if (user) {
<p>Hello, {{ user.name }}</p>
} @else {
<p>Please log in</p>
}
</div>
Lists / Loops
React uses key to help optimize list updates while Angular uses track.
React - Array.map()
function MyComponent() {
const [items, setItems] = useState([
{ id: 1, name: "Apple" },
{ id: 2, name: "Banana" },
{ id: 3, name: "Orange" },
]);
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
Angular - ngFor (Angular 16-)
export class MyComponent {
items = [
{ id: 1, name: "Apple" },
{ id: 2, name: "Banana" },
{ id: 3, name: "Orange" },
];
trackById(index: number, item: any): number {
return item.id;
}
}
<ul>
<li *ngFor="let item of items; trackBy: trackById">{{ item.name }}</li>
</ul>
Angular - @for (Angular 17+)
<ul>
@for (item of items; track item.id) {
<li>{{ item.name }}</li>
}
</ul>
Event Handling
React - Event Binding
In React, an input is considered uncontrolled when its value is accessed via a ref rather than being bound to component state.
function MyComponent() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
};
const handleInput = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value);
};
return (
<div>
<button onClick={handleClick}>Count: {count}</button>
<input onChange={handleInput} />
</div>
);
}
React - Controlled Components
function MyComponent() {
const [inputValue, setInputValue] = useState("");
return (
<input value={inputValue} onChange={(e) => setInputValue(e.target.value)} />
);
}
Angular - Event Binding
export class MyComponent {
count = 0;
handleClick() {
this.count++;
}
handleInput(event: Event) {
const value = (event.target as HTMLInputElement).value;
console.log(value);
}
}
<div>
<button (click)="handleClick()">Count: {{ count }}</button>
<input (input)="handleInput($event)" />
</div>
Angular - Two-Way Binding
export class MyComponent {
inputValue = "";
}
<!-- NgModel (requires FormsModule) -->
<input [(ngModel)]="inputValue" />
<!-- Or manually with property + event binding -->
<input [value]="inputValue" (input)="inputValue = $any($event.target).value" />
Angular - Reactive Forms
Requires ReactiveFormsModule
import { Component } from "@angular/core";
import { FormControl, FormGroup, Validators } from "@angular/forms";
@Component({
selector: "app-login",
templateUrl: "./login.component.html",
})
export class LoginComponent {
form = new FormGroup({
email: new FormControl("", {
nonNullable: true,
validators: [Validators.required, Validators.email],
}),
password: new FormControl("", {
nonNullable: true,
validators: Validators.required,
}),
});
submit() {
if (this.form.valid) {
console.log(this.form.value);
}
}
}
<form [formGroup]="form" (ngSubmit)="submit()">
<input type="email" formControlName="email" />
<input type="password" formControlName="password" />
<button type="submit" [disabled]="form.invalid">Submit</button>
</form>
Data from parents
React - Props
// Parent Component
function ParentComponent() {
return <ChildComponent title="Hello" count={5} />;
}
// Child Component
interface ChildProps {
title: string;
count: number;
}
function ChildComponent({ title, count }: ChildProps) {
return (
<div>
<h1>{title}</h1>
<p>Count: {count}</p>
</div>
);
}
Angular - Inputs (Angular 16-)
// Parent Component
@Component({
selector: "app-parent",
template: '<app-child title="Hello" [count]="5"></app-child>',
})
export class ParentComponent {}
// Child Component
@Component({
selector: "app-child",
template: `
<div>
<h1>{{ title }}</h1>
<p>Count: {{ count }}</p>
</div>
`,
})
export class ChildComponent {
@Input() title!: string;
@Input() count!: number;
}
Angular - Inputs (Angular 17+)
// Child Component with signal inputs
@Component({
selector: "app-child",
template: `
<div>
<h1>{{ title() }}</h1>
<p>Count: {{ count() }}</p>
</div>
`,
})
export class ChildComponent {
title = input.required<string>();
count = input.required<number>();
}
Computed / Derived Values
While React's useMemo and Angular’s computed() can be compared, they are not equivalent.
-
useMemois a performance optimization and does not provide reactive guarantees. -
computed()creates a declarative reactive value that automatically updates when its dependencies change.
React - Derived State
function ShoppingCart() {
const [items, setItems] = useState([
{ name: "Apple", price: 1.5, quantity: 3 },
{ name: "Banana", price: 0.8, quantity: 5 },
]);
const total = useMemo(() => {
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}, [items]);
return <p>Total: ${total.toFixed(2)}</p>;
}
Angular - Getter Methods (Angular 15-)
export class ShoppingCartComponent {
items = [
{ name: "Apple", price: 1.5, quantity: 3 },
{ name: "Banana", price: 0.8, quantity: 5 },
];
get total(): number {
return this.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
}
}
<p>Total: ${{ total | number:'1.2-2' }}</p>
Angular - Computed Signals (Angular 16+)
export class ShoppingCartComponent {
items = signal([
{ name: "Apple", price: 1.5, quantity: 3 },
{ name: "Banana", price: 0.8, quantity: 5 },
]);
total = computed(() => {
return this.items().reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
});
}
<p>Total: ${{ total() | number:'1.2-2' }}</p>
Lifecycle and Side Effects
React - useEffect
function UserProfile({ userId }: { userId: number }) {
const [user, setUser] = useState(null);
useEffect(() => {
// Runs when component mounts or userId changes
fetch(`/api/users/${userId}`)
.then((res) => res.json())
.then((data) => setUser(data));
// Runs when component unmounts
return () => {
console.log("Cleanup");
};
// [userId] defines updates
}, [userId]);
return <div>{user?.name}</div>;
}
Angular - Lifecycle Hooks (Angular 15-)
export class UserProfileComponent implements OnInit, OnDestroy {
@Input() userId!: number;
user: any = null;
constructor(private http: HttpClient) {}
ngOnInit() {
// Runs when component initializes
this.http
.get(`/api/users/${this.userId}`)
.subscribe((data) => (this.user = data));
}
ngOnDestroy() {
// Runs when component is destroyed
console.log("Cleanup");
}
}
Angular - effect() with Signals (Angular 16+)
export class UserProfileComponent {
userId = input.required<number>();
user = signal<any>(null);
constructor() {
// Runs when component initializes
effect((onCleanup) => {
const controller = new AbortController();
// Runs when userId changes
fetch(`/api/users/${this.userId()}`, { signal: controller.signal })
.then((res) => res.json())
.then((data) => this.user.set(data));
// Runs when component is destroyed
onCleanup(() => controller.abort());
});
}
}
I must confess, I’m actually enjoying learning Angular after having stayed away from it for so long. I guess the next step will be the loved/hated RxJS.
Leave a comment if this article sparked your interest in Angular or React, or if you think it can be improved.
And you, what side of the fence are you on? React or Angular?
Top comments (0)