Week 3: Standalone Components & the Angular Mental Model

Before you build anything real, you need a mental model for how an Angular app actually starts up and stays alive. This week answers one question in full: when a browser loads your app, what exactly happens between the blank page and your first rendered component?

Phase 1 of 7 Week 3 of 26 ~3–4 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Explain how a standalone Angular app bootstraps end to end
  • Reason about the component tree as the UI's single source of truth
  • Read an NgModule-based app and know what its standalone equivalent looks like

1. Bootstrapping an App with bootstrapApplication

Everything starts in main.ts. One function call tells Angular "here's my root component, here's my global configuration — go."

main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { App } from './app/app';
import { appConfig } from './app/app.config';

bootstrapApplication(App, appConfig)
  .catch((err) => console.error(err));

Compare that to the older, NgModule-based bootstrap, which you'll still see in production codebases for a while yet:

main.ts — legacy NgModule bootstrap
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';

platformBrowserDynamic().bootstrapModule(AppModule)
  .catch((err) => console.error(err));

Same job, different shape: the NgModule version bootstraps a module that declares a root component; the standalone version bootstraps the component directly, with configuration passed as a plain object instead of module metadata.

2. Standalone Components, Directives & Pipes

A standalone component lists what it needs directly, in its own imports array — no NgModule sitting between it and the pieces it depends on.

app.ts
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { UserCard } from './user-card/user-card';

@Component({
  selector: 'app-root',
  standalone: true, // implicit and optional in current Angular — shown here for clarity
  imports: [RouterOutlet, UserCard],
  templateUrl: './app.html',
})
export class App {}

That imports array is the entire dependency list for this component's template — if UserCard weren't in it, using <app-user-card> in app.html would fail to compile. Directives and pipes work identically: they're just added to the same array.

Why this is better for tree-shaking

Because dependencies are declared per-component instead of per-module, a bundler can trace the exact graph of what's actually used and drop the rest — no more accidentally shipping a whole feature module for one component.

3. Providers & ApplicationConfig

App-wide services — the router, HttpClient, anything you want available everywhere — are registered once, in app.config.ts, and passed into bootstrapApplication.

app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideHttpClient(),
  ],
};

Each provideX() function is a small factory that returns the providers a feature needs — you'll meet provideRouter properly in Week 10 and provideHttpClient in Week 14. For now, the pattern to remember is: app-wide config lives in one array, in one file, instead of scattered across NgModule imports.

4. Change Detection & the Render Cycle (Conceptual Overview)

After bootstrap, Angular needs to know when to re-check your components for changes and update the DOM. Historically, it used a library called Zone.js to patch async browser APIs (setTimeout, event listeners, Promises) so it would automatically know "something happened, time to check for changes."

When a change-detection pass runs, Angular walks the component tree from the root downward, checking each component's template bindings against their current values and patching the DOM wherever they differ.

This gets a full week later

That's the whole picture for now — just enough to know the render cycle exists and runs top-down. Week 5 (Signals) introduces a more precise, dependency-tracked alternative, and Week 20 covers zoneless change detection in depth, including why newer Angular apps are moving away from Zone.js entirely.

5. The Component Tree

Every Angular UI is a tree of components, rooted at whatever you passed to bootstrapApplication. Data generally flows down the tree (parent to child, via inputs) and events flow up (child to parent, via outputs) — you'll build this properly with input()/output() in Week 4, but the shape is worth seeing now:

conceptual tree
App (root)
├── HeaderComponent
├── DashboardComponent
│   ├── UserListComponent
│   │   └── UserCardComponent   (repeated per user)
│   └── StatsPanelComponent
└── FooterComponent

Nothing here is Angular-specific magic — it's the same tree structure as the DOM itself, just with your components as the nodes instead of raw HTML tags. Understanding this tree is what makes state-management decisions in Week 17 intuitive: "how far up the tree does this piece of state need to live?" is the core question you'll keep asking.

6. Migrating a Legacy NgModule App (Conceptual Walkthrough)

You'll encounter NgModule-based codebases in the wild for years to come, so it's worth knowing how the pieces map across. The Angular CLI even ships a schematic that automates most of this:

terminal
ng generate @angular/core:standalone

The mapping it performs, conceptually:

  • @NgModule({ declarations: [...] }) → each declared component/directive/pipe gets standalone: true (implicit today) and its own imports array
  • @NgModule({ imports: [SomeModule] }) → the individual standalone pieces that module used to export are imported directly wherever they're used
  • @NgModule({ providers: [...] }) → moved into ApplicationConfig.providers (for app-wide) or a component's own providers array (for scoped)
  • platformBrowserDynamic().bootstrapModule(AppModule)bootstrapApplication(RootComponent, appConfig)

The schematic runs in three stages — converting declarations, then imports, then removing now-empty NgModules — and it's designed to leave your app working after every stage, so you can migrate incrementally instead of in one risky pass.

7. Hands-on Exercise

Hands-on

Bootstrap a standalone app from scratch — no CLI schematic

Instead of running ng new, hand-write every file involved in getting a three-level component tree on screen.

Requirements:

  1. Set up a minimal project by hand: index.html, main.ts, and a tsconfig.json targeting ES2022 — you can use ng new for the build tooling scaffolding only, then delete everything under src/app and start clean.
  2. Write main.ts calling bootstrapApplication directly, with an inline ApplicationConfig (an empty providers array is fine).
  3. Create three components by hand — AppRoot, DashboardComponent, and StatCardComponent — each with its own imports array, wired so AppRoot renders DashboardComponent, which renders three StatCardComponent instances.
  4. Confirm it runs with ng serve and renders exactly the tree from Section 5's diagram, three levels deep.
Hint

If a component's template isn't rendering, check the parent's imports array first — the single most common standalone-component mistake is using a selector in a template without importing the component that owns it.

8. Knowledge Check

Four quick questions. Expand each to check your answer.

Q1

What two arguments does bootstrapApplication take, and what is each one for?

The root component to render (a class, not an instance), and an ApplicationConfig object holding app-wide providers. Angular creates the root component, attaches it to the DOM element matching its selector in index.html, and makes every provider in the config available throughout the app.

Q2

If <app-user-card> in a template doesn't render, what's the first thing to check?

Whether UserCard is actually listed in the using component's imports array. Standalone components don't inherit availability from anywhere else — every component a template uses has to be imported by that exact component.

Q3

Where do app-wide services like the router or HttpClient get registered in a standalone app?

In the providers array of the ApplicationConfig passed to bootstrapApplication — typically via helper functions like provideRouter() and provideHttpClient(), which each return the exact set of providers that feature needs.

Q4

In the NgModule → standalone migration, what happens to a module's declarations array?

It disappears — there's no standalone equivalent of "declaring" something in a shared list. Each component, directive, or pipe that used to be declared becomes independently standalone, and anywhere it's used gets it added to that consumer's own imports array instead.