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
| Links | React Concept | Angular Equivalent |
|---|---|---|
| ⬇️ | useState |
Component properties / signals |
| ⬇️ | useEffect |
Lifecycle hooks (ngOnInit, ngOnDestroy) / effect()
|
| ⬇️ | Data fetching (useEffect/fetch()/useState) |
HttpClient/effect()
|
| ⬇️ | Props (data / callbacks ) |
@Input() @Output() / signal input output |
| ⬇️ | Context API | @Injectable Service (singleton via DI) |
| ⬇️ | CSS Modules | View Encapsulation |
| ⬇️ | 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>
Lifecycle
Another key aspect of modern frameworks is how they manage a component’s lifecycle, from creation to updates and eventual cleanup.
In React, the lifecycle is driven by rendering and re-rendering. Side effects are handled through hooks such as useEffect, which run in response to render cycles and dependency changes, and cleanup logic is defined within those effects.
In Angular, the lifecycle has traditionally been explicit and structured, with well-defined hooks such as ngOnInit, ngOnChanges, and ngOnDestroy, called by the framework at specific moments. With the introduction of signals and effect(), Angular also supports a more reactive, render-adjacent model, where side effects run automatically in response to state changes rather than fixed lifecycle phases.
React - useEffect
function Component() {
useEffect(() => {
// Run on mount
console.log("Mounted");
// Cleanup function on return
return () => {
console.log("Unmounting");
};
// Re-run when dependency changes
}, [dependency]);
// No dependecy in array, runs just once
// No array as second parameter, re-run every time
return <div>Render</div>;
}
Angular
Lifecycle hooks
@Component({ template: `<div>Render</div>` })
export class Component implements OnInit, OnDestroy, OnChanges {
ngOnInit() {
console.log("Mounted");
}
ngOnChanges(changes: SimpleChanges) {
console.log("Updated", changes);
}
ngOnDestroy() {
console.log("Unmounting");
}
}
Effect (Angular 17+)
@Component({ template: `<div>{{ count() }}</div>` })
export class Component {
constructor() {
// Run on mount and on updates
effect((onCleanup) => {
console.log("Mounted");
// Cleanup function on return
onCleanup(() => console.log("Cleaning up effect"));
});
afterNextRender(() => {
console.log("DOM ready");
});
}
}
Data Fetching
In React, data fetching is commonly performed using the Fetch API, or some other library, inside useEffect and data is then stored in a state using useState.
In Angular, data fetching is typically handled in services using HttpClient, often triggered from lifecycle hooks such as ngOnInit. With signals and effect(), Angular also supports a reactive model where data updates propagate without relying on lifecycle hooks.
React - useEffect + fetch() + useState
function User() {
const [user, setUser] = React.useState(null);
React.useEffect(() => {
fetch("/api/user")
.then((res) => res.json())
.then((data) => setUser(data));
}, []);
return <div>{user?.name}</div>;
}
Angular
HttpClient + lifecycle (ngOnInit)
@Component({
selector: "app-user",
template: "{{ user?.name }}",
})
export class UserComponent implements OnInit {
user: any;
constructor(private http: HttpClient) {}
ngOnInit() {
this.http.get("/api/user").subscribe((data) => {
this.user = data;
});
}
}
HttpClient + signal + effect (Angular 17+)
@Component({
selector: "app-user",
template: "{{ user()?.name }}",
})
export class UserComponent {
user = signal<any>(null);
constructor(private http: HttpClient) {
effect(() => {
this.http.get("/api/user").subscribe((data) => {
this.user.set(data);
});
});
}
}
Global and 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) {}
}
CSS Scoping
Modern frameworks have ways to deal with style encapsulation to avoid conflicting CSS class names and selectors overwriting each other across unrelated components.
React has no built-in CSS scoping but achieves it through its ecosystem. The common approach is creating custom class names to prevent conflicts. CSS cascading is not affected (child components also receive the parent's styles).
Angular creates a custom attribute selector to apply the styles to. CSS cascading is affected (child components do not receive the parent's styles).
React - CSS Modules
function MyComponent() {
return <div className={styles.container} />;
}
<div class="container_a3f2b"></div>
Angular - View Encapsulation
@Component({
selector: "app-my-component",
template: `<div class="container"></div>`,
styles: [
`
.container {
color: blue;
}
`,
],
})
export class MyComponent {}
<div class="container" _ngcontent-abc-123></div>
.container[_ngcontent-abc-123] {
color: blue;
}
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>
Class Binding
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>
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 and 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>
Parent Child Communication
React - Props and Callbacks
// Parent Component
function ParentComponent() {
const [count, setCount] = useState(5);
const handleIncrement = () => {
setCount((prev) => prev + 1);
};
return (
<ChildComponent title="Hello" count={count} onIncrement={handleIncrement} />
);
}
// Child Component
interface ChildProps {
title: string;
count: number;
onIncrement: () => void;
}
function ChildComponent({ title, count, onIncrement }: ChildProps) {
return (
<div>
<h1>{title}</h1>
<p>Count: {count}</p>
<button onClick={onIncrement}>Increment</button>
</div>
);
}
Angular - Inputs and Outputs (Angular 16-)
// Parent Component
@Component({
selector: "app-parent",
template:
'<app-child title="Hello" [count]="count" (increment)="handleIncrement()"></app-child>',
})
export class ParentComponent {
count = 5;
handleIncrement() {
this.count++;
}
}
// Child Component
@Component({
selector: "app-child",
template: `
<div>
<h1>{{ title }}</h1>
<p>Count: {{ count }}</p>
<button (click)="onIncrement()">Increment</button>
</div>
`,
})
export class ChildComponent {
@Input() title!: string;
@Input() count!: number;
@Output() increment = new EventEmitter<void>();
onIncrement() {
this.increment.emit();
}
}
Angular - Signal Inputs and Outputs (Angular 17+)
// Child Component
@Component({
selector: "app-child",
template: `
<div>
<h1>{{ title() }}</h1>
<p>Count: {{ count() }}</p>
<button (click)="onIncrement()">Increment</button>
</div>
`,
})
export class ChildComponent {
title = input.required<string>();
count = input.required<number>();
increment = output<void>();
onIncrement() {
this.increment.emit();
}
}
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>
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)