Chapter 5: Angular - The Framework With Opinions (And a Therapist)
In Defense of Opinions
React’s philosophy: “We’re just a library! We’re unopinionated! You can do whatever you want!”
Six months later, your codebase:
- Three different state management solutions
- Two competing styling approaches
- Four different ways to handle forms
- No two components structured the same way
- A dependency tree that looks like a crime scene
React gave you freedom. Turns out freedom is exhausting.
Angular’s philosophy: “We’re a framework. We have opinions. Here’s how to do things.”
After years of React chaos, Angular’s opinions feel less like constraints and more like… therapy.
The Structure You Didn’t Know You Needed
React project:
src/
components/
Button.jsx
button.styles.js
Button.test.jsx
button.module.css
ButtonContainer.jsx
useButton.js
hooks/
utils/
contexts/
someonejustputfileshere/
Every team invents their own structure. Every project is different. Onboarding is archaeology.
Angular project:
src/
app/
features/
user/
user.component.ts
user.component.html
user.component.css
user.component.spec.ts
user.service.ts
core/
shared/
Everything has a place. Every component follows the same pattern. You can navigate a codebase you’ve never seen because they’re all organized the same way.
It’s like walking into a friend’s kitchen and knowing where the coffee mugs are because everyone sane puts them above the coffee maker.
TypeScript: Not Optional, Actually Good
React: “TypeScript is totally supported! You’ll just need to configure it, type all your props, fight with inference, install DefinitelyTyped packages, and occasionally give up and use any
.”
Angular: “Here’s TypeScript. It works. It’s configured. The CLI generates typed code. Everything is typed. You’re welcome.”
// Angular component
import { Component } from '@angular/core'
@Component({
selector: 'app-counter',
template: `
<button (click)="increment()">
Count: 8
</button>
`
})
export class CounterComponent {
count = 0 // TypeScript infers number
increment() {
this.count++
}
}
No manual typing needed for most things. Decorators tell TypeScript what’s what. Dependency injection is fully typed. Your IDE actually knows what’s going on.
Dependency Injection: Finally, Someone Said It
React’s approach to shared logic:
// Option 1: Props drilling
<GrandParent>
<Parent>
<Child>
<GrandChild>
<GreatGrandChild
userService={userService}
authService={authService}
dataService={dataService}
/>
</GreatGrandChild>
</Child>
</Parent>
</GrandParent>
// Option 2: Context hell
<AuthContext.Provider>
<UserContext.Provider>
<DataContext.Provider>
<ThemeContext.Provider>
<SettingsContext.Provider>
{/* Your actual app, somewhere in here */}
</SettingsContext.Provider>
</ThemeContext.Provider>
</DataContext.Provider>
</UserContext.Provider>
</AuthContext.Provider>
// Option 3: Global state (everyone hates this but we all do it)
import { userService } from './global-state-we-pretend-isnt-global'
Angular’s approach:
// Define a service
@Injectable({
providedIn: 'root'
})
export class UserService {
getUser() { /* ... */ }
}
// Use it anywhere
@Component({ /* ... */ })
export class UserProfile {
constructor(private userService: UserService) {}
ngOnInit() {
this.user = this.userService.getUser()
}
}
Need it? Ask for it in the constructor. Angular provides it. It’s a singleton (or not, if you configure it differently). No Provider wrappers. No context. No prop drilling. Just dependency injection like backend frameworks have had for decades.
“But that’s magic!”
No, it’s a well-established pattern. React chose not to have it, so you reinvented it poorly with Context.
Reactive Forms: Finally, Someone Fixed Forms
React forms are a disaster:
function LoginForm() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [errors, setErrors] = useState({})
const [touched, setTouched] = useState({})
const [isSubmitting, setIsSubmitting] = useState(false)
const validateEmail = (email) => {
if (!email) return 'Required'
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return 'Invalid email'
return null
}
const handleEmailChange = (e) => {
setEmail(e.target.value)
if (touched.email) {
setErrors(prev => ({
...prev,
email: validateEmail(e.target.value)
}))
}
}
const handleEmailBlur = () => {
setTouched(prev => ({ ...prev, email: true }))
setErrors(prev => ({
...prev,
email: validateEmail(email)
}))
}
// ... repeat for password
// ... repeat for every field
// ... cry
}
Or you install Formik, React Hook Form, or one of the 47 other form libraries, each with their own API.
Angular Reactive Forms:
import { Component } from '@angular/core'
import { FormBuilder, Validators } from '@angular/forms'
@Component({
selector: 'app-login',
template: `
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
<input formControlName="email" />
<div *ngIf="loginForm.get('email').invalid && loginForm.get('email').touched">
Invalid email
</div>
<input type="password" formControlName="password" />
<button [disabled]="loginForm.invalid || isSubmitting">
Login
</button>
</form>
`
})
export class LoginComponent {
loginForm = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(8)]]
})
isSubmitting = false
constructor(private fb: FormBuilder) {}
onSubmit() {
if (this.loginForm.valid) {
this.isSubmitting = true
// handle login
}
}
}
Built-in validators. Automatic touched/dirty/valid tracking. Reactive value streams. Nested form groups. It all just works.
RxJS: The Learning Curve With a Payoff
“Angular uses RxJS for everything. It’s so complicated!”
Yes. Initially. Then you realize React developers reinvent bad versions of RxJS patterns constantly:
React async data fetching:
function UserProfile({ userId }) {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
useEffect(() => {
setLoading(true)
setError(null)
fetchUser(userId)
.then(user => {
setUser(user)
setLoading(false)
})
.catch(err => {
setError(err)
setLoading(false)
})
}, [userId])
if (loading) return <Spinner />
if (error) return <Error error={error} />
return <div>{user.name}</div>
}
Race conditions? Hope you added cleanup logic. Debouncing? Install lodash. Cancellation? Good luck.
Angular with RxJS:
@Component({
selector: 'app-user-profile',
template: `
<div *ngIf="user$ | async as user; else loading">
</div>
<ng-template #loading>
<app-spinner></app-spinner>
</ng-template>
`
})
export class UserProfileComponent {
user$ = this.route.params.pipe(
switchMap(params => this.userService.getUser(params.userId)),
catchError(error => of(null))
)
constructor(
private route: ActivatedRoute,
private userService: UserService
) {}
}
The switchMap
automatically cancels previous requests. No race conditions. No manual loading states. The async pipe handles subscription/unsubscription. Built-in error handling.
Common RxJS Patterns
Debounce search:
searchTerm$ = new BehaviorSubject('')
results$ = this.searchTerm$.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(term => this.searchService.search(term))
)
Three lines. In React, you’d need useEffect, useState, setTimeout, cleanup functions, and a prayer.
Combine multiple sources:
viewModel$ = combineLatest([
this.userService.currentUser$,
this.settingsService.settings$,
this.dataService.data$
]).pipe(
map(([user, settings, data]) => ({ user, settings, data }))
)
In React, you’d have three separate useEffects, three loading states, and synchronization nightmares.
Yes, RxJS has a learning curve. But once you get it, async operations become trivial.
Signals: Angular’s New New Hotness
Angular saw the future (Vue, Solid, Svelte) and said “fine, we’ll add signals too”:
import { Component, signal, computed } from '@angular/core'
@Component({
selector: 'app-counter',
template: `
<button (click)="increment()">
Count: 8
Doubled:
</button>
`
})
export class CounterComponent {
count = signal(0)
doubled = computed(() => this.count() * 2)
increment() {
this.count.update(n => n + 1)
}
}
Fine-grained reactivity. Automatic dependency tracking. Better performance. All while keeping the existing Angular patterns for those who want them.
Angular is the only framework that successfully evolved from zone-based change detection to fine-grained reactivity without breaking everything.
The CLI: Actually Useful
React’s CLI experience:
npx create-react-app my-app
# Good luck, you're on your own now
Angular’s CLI experience:
ng new my-app
# Configured project with:
# - TypeScript
# - Testing (Jasmine + Karma)
# - Linting
# - Build optimization
# - Development server
# - Routing
# - Structure
ng generate component user-profile
# Creates:
# - user-profile.component.ts
# - user-profile.component.html
# - user-profile.component.css
# - user-profile.component.spec.ts
# - Updates module imports
ng generate service user
# Creates typed service with DI
# Creates test file
# You're ready to code
The CLI generates good code following best practices. It updates imports. It scaffolds tests. It handles routing config. It’s like having a senior dev on the team.
Migration from React: The Enterprise Approach
Angular migrations are different because Angular is different. You’re not just changing frameworks—you’re adopting an architecture.
Strategy 1: Microfrontends
Run React and Angular side by side:
// Angular shell
import { loadRemoteModule } from '@angular-architects/module-federation'
const routes: Routes = [
{
path: 'react-dashboard',
loadChildren: () => loadRemoteModule({
remoteEntry: 'http://localhost:3000/remoteEntry.js',
remoteName: 'reactApp',
exposedModule: './Dashboard'
})
},
{
path: 'angular-dashboard',
loadChildren: () => import('./dashboard/dashboard.module')
.then(m => m.DashboardModule)
}
]
Different parts of your app can be different frameworks. Useful for gradual migration.
Strategy 2: The Rewrite (For the Brave)
Sometimes React code is so tangled that rewriting is faster than migrating:
React spaghetti:
function Dashboard() {
const [users, setUsers] = useState([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
const [filter, setFilter] = useState('')
const [sortBy, setSortBy] = useState('name')
useEffect(() => {
setLoading(true)
fetchUsers()
.then(data => {
setUsers(data)
setLoading(false)
})
.catch(err => {
setError(err)
setLoading(false)
})
}, [])
const filteredUsers = useMemo(() => {
return users
.filter(u => u.name.includes(filter))
.sort((a, b) => a[sortBy] > b[sortBy] ? 1 : -1)
}, [users, filter, sortBy])
// ... 200 more lines
}
Angular clarity:
@Component({
selector: 'app-dashboard',
templateUrl: './dashboard.component.html'
})
export class DashboardComponent implements OnInit {
users$ = this.userService.getUsers().pipe(
map(users => this.filterAndSort(users)),
catchError(error => {
this.errorService.handle(error)
return of([])
})
)
filter = signal('')
sortBy = signal('name')
constructor(
private userService: UserService,
private errorService: ErrorService
) {}
private filterAndSort(users: User[]) {
return users
.filter(u => u.name.includes(this.filter()))
.sort((a, b) => a[this.sortBy()] > b[this.sortBy()] ? 1 : -1)
}
}
Sometimes starting fresh with better architecture is the move.
Converting Common Patterns
State → Service:
// React: useState everywhere
// Angular: Centralized service
@Injectable({ providedIn: 'root' })
export class UserStateService {
private usersSubject = new BehaviorSubject<User[]>([])
users$ = this.usersSubject.asObservable()
updateUsers(users: User[]) {
this.usersSubject.next(users)
}
}
Context → Service:
// React: Context Provider
// Angular: Just inject the service
// Any component can inject it
constructor(private userState: UserStateService) {}
useEffect → Lifecycle hooks + RxJS:
// React: useEffect
// Angular: ngOnInit + subscriptions
ngOnInit() {
this.userService.getUser().subscribe(user => {
this.user = user
})
}
// Or better:
user$ = this.userService.getUser()
When Angular Makes Sense
Angular excels at:
- Enterprise applications - The structure scales beautifully
- Large teams - Consistency across the codebase
- Complex forms - Built-in form handling is unmatched
- Long-term projects - Angular evolves without breaking things
- Teams that want guidance - The framework tells you the right way
Angular might not fit:
- Tiny projects - The structure is overkill for a landing page
- Teams that hate opinions - Angular has lots of them
- Developers allergic to learning - There’s a real learning curve
- Projects that need bleeding-edge experimental features - Angular is stable, not experimental
The Learning Curve Reality
Week 1: “This is so much more complicated than React!” Week 2: “Okay, dependency injection makes sense.” Week 3: “Reactive forms are actually amazing.” Week 4: “RxJS is starting to click.” Month 2: “I can’t believe React doesn’t have DI.” Month 3: “Every React codebase feels chaotic now.” Month 6: “I’m productive and the code is maintainable.”
Angular takes longer to learn. But the investment pays off in:
- Consistent, maintainable codebases
- Better tools out of the box
- Less “which library should we use?” debates
- Actual architectural guidance
Real Talk: Angular vs React
React: “Move fast and figure it out yourself.” Angular: “Here’s how to build maintainable applications.”
React optimizes for initial productivity. Get something working fast, worry about architecture later (or never).
Angular optimizes for long-term maintainability. Spend time learning upfront, coast on good architecture forever.
Different goals. Different trade-offs.
If you’re building Facebook, React makes sense. If you’re building enterprise software that will be maintained for years, Angular might be the adult choice.
“I switched to Angular and suddenly had opinions about folder structure. Turns out opinions prevent chaos.” — A Developer Who Found Structure
Up Next: Chapter 6: Alpine.js - When You Just Need to Sprinkle Some Magic