# Client-Side Rendering

Course: [Angular](https://www.devtutorials.ai/learn/angular) · Section: Render Modes
Source: https://www.devtutorials.ai/learn/angular/rendering/client-side-rendering

> How Angular renders in the browser: bootstrap, initial rendering, re-rendering and its triggers, the advantages and limitations of CSR, and a Product Catalog capstone project.

## Introduction to Rendering

**Rendering** means creating the HTML that the user sees on the screen. In an Angular app, Angular renders your components: it turns each template and its data into HTML elements.

The important question is **where** and **when** the HTML is created. Angular supports three ways, called **render modes**:

| Render mode | Where the HTML is created | When |
|---|---|---|
| Client-Side Rendering (CSR) | In the user's browser | When the user opens the page |
| Server-Side Rendering (SSR) | On the server | For each request |
| Static Site Generation (SSG) | On your computer or build server | Once, when you build the app |

This lesson covers CSR. The next two lessons cover SSR and SSG.

## Client-Side Rendering

In **Client-Side Rendering (CSR)**, the server sends an almost empty HTML page and the JavaScript files of the app. The browser runs the JavaScript, and Angular creates all the HTML in the browser.

The **client** is the user's browser. CSR is the default: every app in this course so far used CSR.

### Real-World Example

A company's sales dashboard is used only by employees after they log in. The browser downloads the Angular app once. After that, filtering orders, switching tabs and opening order details all happen in the browser, without new pages from the server.

Search engines never need to read the dashboard, so CSR is a good fit.

<figure className="ngc-fig">
<svg viewBox="0 0 640 140" role="img" aria-label="Client-side rendering: an empty index.html, then the JavaScript downloads, Angular renders in the browser, and the page becomes visible.">
<defs><marker id="ngc-c1" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0L10 5L0 10z" className="ngc-arrowhead" /></marker></defs><g className="ngc-step ngc-t1"><g className="ngc-node ngc-soft c-blue"><rect x="6" y="36" width="142" height="64" rx="9" /><text x="77.0" y="65" textAnchor="middle" style={{fontSize:"14px"}}>index.html</text><text className="ngc-sub" x="77.0" y="83" textAnchor="middle">empty &lt;app-root&gt;</text></g></g><line className="ngc-link ngc-flow c-slate" x1="150" y1="68" x2="162" y2="68" markerEnd="url(#ngc-c1)" /><g className="ngc-step ngc-t2"><g className="ngc-node ngc-soft c-orange"><rect x="164" y="36" width="142" height="64" rx="9" /><text x="235.0" y="65" textAnchor="middle" style={{fontSize:"14px"}}>JavaScript</text><text className="ngc-sub" x="235.0" y="83" textAnchor="middle">downloads</text></g></g><line className="ngc-link ngc-flow c-slate" x1="308" y1="68" x2="320" y2="68" markerEnd="url(#ngc-c1)" /><g className="ngc-step ngc-t3"><g className="ngc-node ngc-soft c-purple"><rect x="322" y="36" width="142" height="64" rx="9" /><text x="393.0" y="65" textAnchor="middle" style={{fontSize:"14px"}}>Angular</text><text className="ngc-sub" x="393.0" y="83" textAnchor="middle">renders in browser</text></g></g><line className="ngc-link ngc-flow c-slate" x1="466" y1="68" x2="478" y2="68" markerEnd="url(#ngc-c1)" /><g className="ngc-step ngc-t4"><g className="ngc-node ngc-soft c-green"><rect x="480" y="36" width="142" height="64" rx="9" /><text x="551.0" y="65" textAnchor="middle" style={{fontSize:"14px"}}>Page visible</text><text className="ngc-sub" x="551.0" y="83" textAnchor="middle">content + data</text></g></g><text className="ngc-note" x="320" y="124" textAnchor="middle">The user sees an empty page until Angular has rendered in the browser.</text>
</svg>
<figcaption>Client-Side Rendering: the browser creates the HTML.</figcaption>
</figure>

## CSR Architecture

### Angular Application Bootstrap

**Bootstrap** means starting the app. The browser loads `index.html`, which contains only the root element:

```html
<body>
  <app-root></app-root>
</body>
```

Then it runs `main.ts`, which starts Angular with the root component and the app configuration:

```ts
// main.ts
bootstrapApplication(App, appConfig);
```

### Browser Rendering

Angular finds `<app-root>` in the page and renders the `App` component inside it. Then it renders every child component in the `App` template, one level after another.

All of this happens in the browser. The server only sent files. It did not create any of the visible HTML.

### Root Component Initialization

When Angular creates a component, it follows a fixed order, which you learned in the Component Lifecycle Hook lesson:

1. It runs the **constructor**. Services are injected here.
2. It sets the `@Input()` values.
3. It calls **`ngOnInit()`**.
4. It renders the template.

The next example writes a line at each step.

```angular runnable title="The order of initialization"
import { Component, Injectable, Input, OnInit, inject, signal } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class StartupLog {
  readonly lines = signal<string[]>([]);
  add(line: string) {
    this.lines.update(list => [...list, line]);
  }
}

@Component({
  selector: 'app-header',
  template: `<p><b>{{ title }}</b></p>`,
})
export class Header implements OnInit {
  @Input() title = '';
  private log = inject(StartupLog);

  constructor() {
    this.log.add('3. Header: constructor');
  }

  ngOnInit() {
    this.log.add(`4. Header: ngOnInit, title = "${this.title}"`);
  }
}

@Component({
  selector: 'app-root',
  imports: [Header],
  template: `
    <app-header title="My Store" />
    @for (line of log.lines(); track $index) {
      <p>{{ line }}</p>
    }
  `,
})
export class App implements OnInit {
  log = inject(StartupLog);

  constructor() {
    this.log.add('1. App: constructor');
  }

  ngOnInit() {
    this.log.add('2. App: ngOnInit');
  }
}
```

`App` is initialized first. Then Angular renders its template and creates `Header`, with its `title` input already set in `ngOnInit()`.

## Initial Rendering

The **initial rendering** is the first time Angular creates the HTML of the page. In CSR, the user sees nothing useful until these steps are complete:

1. The browser downloads `index.html`. The page is empty.
2. The browser downloads the JavaScript files.
3. Angular starts and renders the components.
4. Components request their data from the server. Until it arrives, they show a loading text.
5. The data arrives, and Angular renders it.

```angular runnable title="Initial rendering with data from a server"
import { ApplicationConfig, Component, inject, signal } from '@angular/core';
import { HttpClient, HttpHandlerFn, HttpRequest, HttpResponse, provideHttpClient, withInterceptors } from '@angular/common/http';
import { delay, of } from 'rxjs';

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

@Component({
  selector: 'app-root',
  template: `
    <h4>Products</h4>
    @if (loading()) {
      <p>Loading...</p>
    } @else {
      @for (product of products(); track product.id) {
        <p>{{ product.name }}: {{ product.price }}</p>
      }
    }
  `,
})
export class App {
  products = signal<any[]>([]);
  loading = signal(true);

  constructor() {
    inject(HttpClient)
      .get<any[]>('/api/products')
      .subscribe(list => {
        this.products.set(list);
        this.loading.set(false);
      });
  }
}
// ─── Practice server ──────────────────────────────────────────────
// Answers '/api/products' inside the browser, like in the API Integration lesson.
const PRODUCTS = [
  { id: 1, name: 'Wireless Mouse', category: 'Accessories', price: 15 },
  { id: 2, name: 'Mechanical Keyboard', category: 'Accessories', price: 40 },
  { id: 3, name: '27-inch Monitor', category: 'Displays', price: 180 },
  { id: 4, name: 'Portable Monitor', category: 'Displays', price: 120 },
  { id: 5, name: 'Headphones', category: 'Audio', price: 95 },
  { id: 6, name: 'Bluetooth Speaker', category: 'Audio', price: 35 },
];

function practiceServer(req: HttpRequest<unknown>, next: HttpHandlerFn) {
  return of(new HttpResponse({ status: 200, body: PRODUCTS })).pipe(delay(300));
}

```

Click **Run** and watch the output. "Loading..." appears first, and the list replaces it when the data arrives.

Until step 3 finishes, the user sees an empty page. On a slow phone or network, this can take several seconds. This is the main limitation of CSR, and the reason SSR and SSG exist.

## Component Re-rendering

After the initial rendering, the page does not stay the same. **Re-rendering** means that Angular updates the HTML when data changes.

Angular does not create the whole page again. It checks the templates and changes **only** the elements whose values changed. For example, when a counter changes from 1 to 2, Angular changes only that number in the page.

Angular checks a component again when something tells it that data may have changed. These are the **rendering triggers**.

In the next example, each click changes one quantity. Angular updates only the number in that row. The other row and the heading are not created again.

```angular runnable title="Only the changed value is updated"
import { Component, signal } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <h4>Your cart</h4>
    <p>Mouse × {{ mouse() }} <button (click)="mouse.update(n => n + 1)">+1</button></p>
    <p>Cable × {{ cable() }} <button (click)="cable.update(n => n + 1)">+1</button></p>
  `,
})
export class App {
  mouse = signal(1);
  cable = signal(2);
}
```

Open the browser's developer tools (F12), select the **Elements** tab and click a **+1** button. Only the changed number flashes, because only this text was updated.

## Rendering Triggers

### User Events

<figure className="ngc-fig">
<svg viewBox="0 0 640 200" role="img" aria-label="User events, signal changes and data updates start change detection, which updates only the changed elements.">
<defs><marker id="ngc-r3" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0L10 5L0 10z" className="ngc-arrowhead" /></marker></defs><g className="ngc-step ngc-t1"><g className="ngc-node ngc-soft c-blue"><rect x="10" y="8" width="190" height="50" rx="9" /><text x="105.0" y="30" textAnchor="middle" style={{fontSize:"14px"}}>User event</text><text className="ngc-sub" x="105.0" y="48" textAnchor="middle">(click), (input)</text></g></g><line className="ngc-link ngc-flow c-slate" x1="202" y1="33" x2="286" y2="92" markerEnd="url(#ngc-r3)" /><g className="ngc-step ngc-t2"><g className="ngc-node ngc-soft c-purple"><rect x="10" y="70" width="190" height="50" rx="9" /><text x="105.0" y="92" textAnchor="middle" style={{fontSize:"14px"}}>State change</text><text className="ngc-sub" x="105.0" y="110" textAnchor="middle">signal.set()</text></g></g><line className="ngc-link ngc-flow c-slate" x1="202" y1="95" x2="286" y2="92" markerEnd="url(#ngc-r3)" /><g className="ngc-step ngc-t3"><g className="ngc-node ngc-soft c-orange"><rect x="10" y="132" width="190" height="50" rx="9" /><text x="105.0" y="154" textAnchor="middle" style={{fontSize:"14px"}}>Data update</text><text className="ngc-sub" x="105.0" y="172" textAnchor="middle">HTTP answer</text></g></g><line className="ngc-link ngc-flow c-slate" x1="202" y1="157" x2="286" y2="92" markerEnd="url(#ngc-r3)" /><g className="ngc-step ngc-t4"><g className="ngc-node ngc-solid c-red"><rect x="290" y="62" width="160" height="60" rx="9" /><text x="370.0" y="89" textAnchor="middle" style={{fontSize:"14px"}}>Change detection</text><text className="ngc-sub" x="370.0" y="107" textAnchor="middle">checks templates</text></g></g><line className="ngc-link ngc-flow c-slate" x1="452" y1="92" x2="486" y2="92" markerEnd="url(#ngc-r3)" /><g className="ngc-step ngc-t4"><g className="ngc-node ngc-soft c-green"><rect x="488" y="62" width="142" height="60" rx="9" /><text x="559.0" y="89" textAnchor="middle" style={{fontSize:"14px"}}>DOM updated</text><text className="ngc-sub" x="559.0" y="107" textAnchor="middle">only changed values</text></g></g>
</svg>
<figcaption>Three triggers start a re-render. Angular changes only what changed.</figcaption>
</figure>

A user event in a template, such as `(click)` or `(input)`, tells Angular to check the component after the event code runs.

### State Changes

When a signal that a template reads gets a new value, Angular re-renders that template. This works for every change: after a click, a timer or a server answer.

### Data Updates

When data arrives from a server, the code stores it. If it is stored in a signal, or shown with the `async` pipe, Angular re-renders the page with the new data.

Show a loading text while data is requested, and an error message if the request fails. Without them, the user sees an empty area and cannot know if the app is still working.

The next example has all three triggers.

```angular runnable title="Three rendering triggers"
import { ApplicationConfig, Component, inject, signal } from '@angular/core';
import { HttpClient, HttpHandlerFn, HttpRequest, HttpResponse, provideHttpClient, withInterceptors } from '@angular/common/http';
import { delay, of } from 'rxjs';

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

@Component({
  selector: 'app-root',
  template: `
    <p>1. User event: clicked {{ clicks() }} times
      <button (click)="clicks.update(n => n + 1)">Click me</button></p>
    <p>2. State change: {{ seconds() }} seconds since start</p>
    <p>3. Data update: {{ productCount() }} products loaded</p>
  `,
})
export class App {
  clicks = signal(0);
  seconds = signal(0);
  productCount = signal(0);

  constructor() {
    setInterval(() => this.seconds.update(s => s + 1), 1000);
    inject(HttpClient)
      .get<any[]>('/api/products')
      .subscribe(list => this.productCount.set(list.length));
  }
}
// ─── Practice server ──────────────────────────────────────────────
// Answers '/api/products' inside the browser, like in the API Integration lesson.
const PRODUCTS = [
  { id: 1, name: 'Wireless Mouse', category: 'Accessories', price: 15 },
  { id: 2, name: 'Mechanical Keyboard', category: 'Accessories', price: 40 },
  { id: 3, name: '27-inch Monitor', category: 'Displays', price: 180 },
  { id: 4, name: 'Portable Monitor', category: 'Displays', price: 120 },
  { id: 5, name: 'Headphones', category: 'Audio', price: 95 },
  { id: 6, name: 'Bluetooth Speaker', category: 'Audio', price: 35 },
];

function practiceServer(req: HttpRequest<unknown>, next: HttpHandlerFn) {
  return of(new HttpResponse({ status: 200, body: PRODUCTS })).pipe(delay(300));
}

```

A normal field changed by a timer or a server answer does not re-render the page. Store such values in signals, or show them with the `async` pipe.

## Advantages and Limitations of CSR

**Advantages:**

- **Fast after the first load.** Moving between screens needs no new pages from the server.
- **Simple hosting.** The server only sends files, so any file hosting service works.
- **Low server cost.** All rendering work happens in the users' browsers.
- **Very interactive.** Good for dashboards, editors and tools that users work in for a long time.

**Limitations:**

- **Slow first view.** The user sees an empty page until the JavaScript has downloaded and run.
- **Weaker for search engines.** Some search engines and link previews (for example in chat apps) read only the first HTML, which is empty.
- **Depends on the user's device.** A slow phone renders the app slowly.

| Use CSR for | Consider SSR or SSG for |
|---|---|
| Apps behind a login, such as dashboards | Public pages that search engines must find |
| Tools that users keep open | Pages where the first view must be fast |
| Internal company apps | Blogs, news, shops, documentation |

## Capstone Project: Product Catalog

Now you build a **Product Catalog** with CSR. It combines components, signals, state in a service and `HttpClient`.

### Build Reusable Product Components

`ProductCard` displays one product. It receives the product with `@Input()` and reports the **Add to cart** click with `@Output()`. The same component is used for every product.

### Implement Search Functionality

`ProductStore` stores the search text in a signal. The search box calls `setSearch()` on every key press.

### Implement Category Filtering

The category buttons call `setCategory()`. A computed signal `visibleProducts` uses both the search text and the category.

### Manage Application State

All state is in `ProductStore`: the products, the search text, the category and the cart. The writable signals are private, and components change them only with the store's methods.

### Handle User Interactions

Typing, clicking a category and clicking **Add to cart** are user events. Each one calls a store method, and the signals re-render the page.

### Display Dynamic Data

The products come from the server with `HttpClient`. The page shows "Loading..." until they arrive.

```angular runnable title="Capstone: Product Catalog"
import { ApplicationConfig, Component, EventEmitter, Injectable, Input, Output, computed, inject, signal } from '@angular/core';
import { HttpClient, HttpHandlerFn, HttpRequest, HttpResponse, provideHttpClient, withInterceptors } from '@angular/common/http';
import { delay, of } from 'rxjs';

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

export interface Product {
  id: number;
  name: string;
  category: string;
  price: number;
}

// ── State ──
@Injectable({ providedIn: 'root' })
export class ProductStore {
  private http = inject(HttpClient);
  private readonly _products = signal<Product[]>([]);
  private readonly _search = signal('');
  private readonly _category = signal('All');
  private readonly _cart = signal<Product[]>([]);
  readonly loading = signal(true);

  readonly category = this._category.asReadonly();
  readonly cartCount = computed(() => this._cart().length);
  readonly categories = computed(() => ['All', ...new Set(this._products().map(p => p.category))]);
  readonly visibleProducts = computed(() => {
    const text = this._search().toLowerCase();
    const category = this._category();
    return this._products().filter(
      p => (category === 'All' || p.category === category) && p.name.toLowerCase().includes(text),
    );
  });

  load() {
    this.http.get<Product[]>('/api/products').subscribe(list => {
      this._products.set(list);
      this.loading.set(false);
    });
  }
  setSearch(text: string) {
    this._search.set(text);
  }
  setCategory(category: string) {
    this._category.set(category);
  }
  addToCart(product: Product) {
    this._cart.update(list => [...list, product]);
  }
}

// ── Reusable component ──
@Component({
  selector: 'app-product-card',
  template: `
    <p>
      <b>{{ product.name }}</b> ({{ product.category }}) {{ product.price }}
      <button (click)="add.emit(product)">Add to cart</button>
    </p>
  `,
})
export class ProductCard {
  @Input() product!: Product;
  @Output() add = new EventEmitter<Product>();
}

// ── Page ──
@Component({
  selector: 'app-root',
  imports: [ProductCard],
  template: `
    <p>🛒 Cart: {{ store.cartCount() }}</p>
    <input placeholder="Search" (input)="store.setSearch($any($event.target).value)" />
    <p>
      @for (category of store.categories(); track category) {
        <button (click)="store.setCategory(category)" [disabled]="category === store.category()">{{ category }}</button>
      }
    </p>
    @if (store.loading()) {
      <p>Loading...</p>
    } @else {
      @for (product of store.visibleProducts(); track product.id) {
        <app-product-card [product]="product" (add)="store.addToCart($event)" />
      } @empty {
        <p>No products match your search.</p>
      }
    }
  `,
})
export class App {
  store = inject(ProductStore);
  constructor() {
    this.store.load();
  }
}
// ─── Practice server ──────────────────────────────────────────────
// Answers '/api/products' inside the browser, like in the API Integration lesson.
const PRODUCTS = [
  { id: 1, name: 'Wireless Mouse', category: 'Accessories', price: 15 },
  { id: 2, name: 'Mechanical Keyboard', category: 'Accessories', price: 40 },
  { id: 3, name: '27-inch Monitor', category: 'Displays', price: 180 },
  { id: 4, name: 'Portable Monitor', category: 'Displays', price: 120 },
  { id: 5, name: 'Headphones', category: 'Audio', price: 95 },
  { id: 6, name: 'Bluetooth Speaker', category: 'Audio', price: 35 },
];

function practiceServer(req: HttpRequest<unknown>, next: HttpHandlerFn) {
  return of(new HttpResponse({ status: 200, body: PRODUCTS })).pipe(delay(300));
}

```

Type "monitor" in the search box, click a category, and add products to the cart.

`$any($event.target).value` reads the text of the input. `$any()` tells TypeScript not to check the type of `$event.target`.

## Download Project Files

### Download CSR Project Source Code

<a href="/downloads/angular-product-catalog-csr.zip" download>Download angular-product-catalog-csr.zip</a>

The ZIP file contains the Product Catalog as a complete Angular CLI project, with each component in its own file.

| File | What it contains |
|---|---|
| `src/app/product.model.ts` | The `Product` interface |
| `src/app/product-store.ts` | All state: products, search, category and cart |
| `src/app/components/product-card.ts` | The reusable product card |
| `src/app/components/search-box.ts` | The search input |
| `src/app/components/category-filter.ts` | The category buttons |
| `src/app/components/cart-summary.ts` | The cart count and total |
| `public/products.json` | The product data that the app loads with `HttpClient` |

### Project Setup Instructions

You need **Node.js 20.19 or newer**. Check your version with `node -v`.

1. Unzip the file and open a terminal in the `product-catalog` folder.
2. Install the packages:

   ```bash
   npm install
   ```

3. Start the app:

   ```bash
   npm start
   ```

4. Open `http://localhost:4200`.

To see that this is CSR, open the page source in the browser (right-click → **View Page Source**). The HTML contains only `<app-root></app-root>`, without any products.

## Common Mistakes

<div className="ngc-mistakes">

| Mistake | What you see | Fix |
|---|---|---|
| A timer or server answer changes a normal field | The page does not update | Store the value in a signal |
| No loading text while data is requested | An empty area, and users think the page is broken | Show a loading message until the data arrives |
| Using CSR for public pages that must appear in search engines | Search previews are empty | Use SSR or SSG for those pages |
| Very large JavaScript files | A long empty page on slow phones | Lazy-load pages that users rarely open |

</div>

## Check Yourself

## Exercises

## What to Remember

- Rendering creates the HTML of the page. In CSR, the server sends an almost empty page and the JavaScript, and Angular renders everything in the browser.
- Angular starts with `bootstrapApplication()`, renders the root component into `<app-root>`, and re-renders only what changed after user events, signal changes and data updates.
- CSR is fast after the first load and simple to host, but the first view is slow and search engines may see an empty page.

Next: **Server-Side Rendering**, how the server creates the HTML so the first view is fast.
