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:

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:

Angular might not fit:

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:

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