# Client-Side Rendering

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

> What rendering is, how client-side rendering works, CSR architecture and startup, initial render, re-rendering and the render cycle, the Virtual DOM and diffing, rendering triggers, and a Product Catalog capstone.

## Introduction to Rendering

**Rendering** means turning your components into the HTML that the user sees on the screen.

Your code describes **what** the page should look like. Rendering is the work that makes it real. This work can happen in two places:

- in the **browser** (the client): this is **client-side rendering (CSR)**, the topic of this lesson.
- on the **server**, before the page is sent: this is **server-side rendering (SSR)**, the next lesson.

The choice of where and when to render is called the **render mode**.

<figure className="react-fig">
<svg viewBox="0 0 520 110" role="img" aria-labelledby="fig-csr-what" style={{ width: "100%", maxWidth: 520, height: "auto" }}>
  <title id="fig-csr-what">Rendering turns components into what you see on the screen</title>
  <rect x="10" y="30" width="140" height="50" rx="8" style={{ fill: "var(--accent-soft)", stroke: "var(--accent)", strokeWidth: 1.5 }} />
  <text x="80.0" y="52.0" textAnchor="middle" style={{ fill: "var(--accent)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>Components</text>
  <text x="80.0" y="69.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>your TSX code</text>
  <path d="M150 55 L190 55" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M190 55 L181.7 58.5 L181.7 51.5 Z" style={{ fill: "var(--faint)" }} />
  <rect x="195" y="30" width="130" height="50" rx="8" style={{ fill: "var(--accent-secondary-soft)", stroke: "var(--accent-secondary)", strokeWidth: 1.5 }} />
  <text x="260.0" y="52.0" textAnchor="middle" style={{ fill: "var(--accent-secondary)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Render</text>
  <text x="260.0" y="69.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>React works</text>
  <path d="M325 55 L365 55" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M365 55 L356.7 58.5 L356.7 51.5 Z" style={{ fill: "var(--faint)" }} />
  <rect x="370" y="30" width="140" height="50" rx="8" style={{ fill: "var(--ok-soft)", stroke: "var(--ok)", strokeWidth: 1.5 }} />
  <text x="440.0" y="52.0" textAnchor="middle" style={{ fill: "var(--ok)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Screen</text>
  <text x="440.0" y="69.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>HTML the user sees</text>
  <circle r="6" style={{ fill: "var(--accent-secondary)" }}>
    <animateMotion dur="2.4s" repeatCount="indefinite" path="M150 55 L190 55 M325 55 L365 55" />
  </circle>
</svg>
<figcaption>Rendering is the step between your code and the page.</figcaption>
</figure>

## Introduction to Client-Side Rendering

In **client-side rendering**, the server sends an almost empty HTML page and a JavaScript file. The browser runs the JavaScript, and React builds the whole page there.

Every app in this course so far uses CSR. A normal Vite + React app is a CSR app.

### Real-World Example

A bookshop admin dashboard is used only by signed-in staff. The server sends an almost empty `index.html` and a JavaScript bundle. React runs in the browser, requests the orders with `fetch`, and builds the tables and charts.

Search engines do not need to read this page, so CSR is a good choice. After the first load, every screen change is instant.

<figure className="react-fig">
<svg viewBox="0 0 520 150" role="img" aria-labelledby="fig-csr-flow" style={{ width: "100%", maxWidth: 520, height: "auto" }}>
  <title id="fig-csr-flow">Client-side rendering: the server sends an almost empty page and a JavaScript file, and React builds the page in the browser</title>
  <rect x="5" y="20" width="95" height="50" rx="8" style={{ fill: "var(--accent-soft)", stroke: "var(--accent)", strokeWidth: 1.5 }} />
  <text x="52.5" y="42.0" textAnchor="middle" style={{ fill: "var(--accent)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Browser</text>
  <text x="52.5" y="59.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>asks for page</text>
  <path d="M100 45 L130 45" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M130 45 L121.7 48.5 L121.7 41.5 Z" style={{ fill: "var(--faint)" }} />
  <rect x="135" y="20" width="115" height="50" rx="8" style={{ fill: "var(--panel2)", stroke: "var(--faint)", strokeWidth: 1.5 }} />
  <text x="192.5" y="42.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Server</text>
  <text x="192.5" y="59.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>sends files</text>
  <path d="M250 45 L280 45" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M280 45 L271.7 48.5 L271.7 41.5 Z" style={{ fill: "var(--faint)" }} />
  <rect x="285" y="10" width="110" height="30" rx="8" style={{ fill: "var(--panel2)", stroke: "var(--faint)", strokeWidth: 1.5 }} />
  <text x="340.0" y="30.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>index.html</text>
  <rect x="285" y="48" width="110" height="30" rx="8" style={{ fill: "var(--accent-secondary-soft)", stroke: "var(--accent-secondary)", strokeWidth: 1.5 }} />
  <text x="340.0" y="68.0" textAnchor="middle" style={{ fill: "var(--accent-secondary)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>app.js</text>
  <path d="M395 45 L420 45" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M420 45 L411.7 48.5 L411.7 41.5 Z" style={{ fill: "var(--faint)" }} />
  <rect x="425" y="20" width="90" height="50" rx="8" style={{ fill: "var(--ok-soft)", stroke: "var(--ok)", strokeWidth: 1.5 }} />
  <text x="470.0" y="42.0" textAnchor="middle" style={{ fill: "var(--ok)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>React</text>
  <text x="470.0" y="59.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>builds page</text>
  <text x="340" y="100" textAnchor="middle" style={{ fill: "var(--muted)", fontSize: 12, fontWeight: 400, fontFamily: "var(--mono)" }}>index.html has only &lt;div id="root"&gt;&lt;/div&gt;</text>
  <text x="260" y="130" textAnchor="middle" style={{ fill: "var(--bad)", fontSize: 12, fontWeight: 700, fontFamily: "var(--sans)" }}>The page is blank until app.js has loaded and run</text>
  <circle r="6" style={{ fill: "var(--accent-secondary)" }}>
    <animateMotion dur="3s" repeatCount="indefinite" path="M100 45 L130 45 M250 45 L280 45 M395 45 L420 45" />
  </circle>
</svg>
<figcaption>All the HTML is made by JavaScript in the browser.</figcaption>
</figure>

This is the whole body of a Vite app's `index.html`. Only one empty `div`:

```html
<body>
  <div id="root"></div>
  <script type="module" src="/src/main.tsx"></script>
</body>
```

## Benefits of CSR

- **Fast after the first load.** Moving between pages does not reload the page. React only changes what is needed.
- **Feels like an app.** Clicks, typing and filters update the screen at once.
- **Simple to host.** The build is just static files (HTML, JS, CSS). Any static host or CDN can serve them.
- **Light server work.** The server only sends files. Each user's browser does its own rendering.

## CSR Architecture

React is split into parts. Each part has one job:

- **Your components** describe the screen.
- **React** (the `react` package) calls your components and builds a tree of elements, the **Virtual DOM**.
- The **reconciler** compares the new tree with the old one.
- **ReactDOM** (the `react-dom` package) changes the real page to match.

<figure className="react-fig">
<svg viewBox="0 0 520 120" role="img" aria-labelledby="fig-csr-arch" style={{ width: "100%", maxWidth: 520, height: "auto" }}>
  <title id="fig-csr-arch">React rendering architecture: components make React elements, the reconciler compares them, and ReactDOM updates the real DOM</title>
  <rect x="5" y="35" width="95" height="50" rx="8" style={{ fill: "var(--accent-soft)", stroke: "var(--accent)", strokeWidth: 1.5 }} />
  <text x="52.5" y="57.0" textAnchor="middle" style={{ fill: "var(--accent)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Components</text>
  <text x="52.5" y="74.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>your code</text>
  <path d="M100 60 L120 60" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M120 60 L111.7 63.5 L111.7 56.5 Z" style={{ fill: "var(--faint)" }} />
  <rect x="125" y="35" width="115" height="50" rx="8" style={{ fill: "var(--accent-secondary-soft)", stroke: "var(--accent-secondary)", strokeWidth: 1.5 }} />
  <text x="182.5" y="57.0" textAnchor="middle" style={{ fill: "var(--accent-secondary)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Virtual DOM</text>
  <text x="182.5" y="74.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>elements</text>
  <path d="M240 60 L260 60" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M260 60 L251.7 63.5 L251.7 56.5 Z" style={{ fill: "var(--faint)" }} />
  <rect x="265" y="35" width="115" height="50" rx="8" style={{ fill: "var(--info-soft)", stroke: "var(--info)", strokeWidth: 1.5 }} />
  <text x="322.5" y="57.0" textAnchor="middle" style={{ fill: "var(--info)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Reconciler</text>
  <text x="322.5" y="74.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>finds changes</text>
  <path d="M380 60 L400 60" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M400 60 L391.7 63.5 L391.7 56.5 Z" style={{ fill: "var(--faint)" }} />
  <rect x="405" y="35" width="110" height="50" rx="8" style={{ fill: "var(--ok-soft)", stroke: "var(--ok)", strokeWidth: 1.5 }} />
  <text x="460.0" y="57.0" textAnchor="middle" style={{ fill: "var(--ok)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>ReactDOM</text>
  <text x="460.0" y="74.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>real DOM</text>
  <circle r="6" style={{ fill: "var(--accent-secondary)" }}>
    <animateMotion dur="3s" repeatCount="indefinite" path="M100 60 L120 60 M240 60 L260 60 M380 60 L400 60" />
  </circle>
</svg>
<figcaption>React (the core) works out what changed. ReactDOM does the changing.</figcaption>
</figure>

### ReactDOM and Root Element

The **root element** is the HTML element where React draws your app, usually `<div id="root">`. React controls everything inside it.

Two lines connect React to the page:

```tsx
const root = createRoot(document.getElementById("root")!);
root.render(<App />);
```

- `createRoot(element)` makes a React root inside that element.
- `root.render(<App />)` draws `App` into it.
- The `!` tells TypeScript "this element exists, it is not null".

A page can have more than one root, for example to add React to a small part of an older website. Below, `main.tsx` makes two roots.

      Small widget (root 2) <button onClick={() => setLikes(likes + 1)}>Like {likes}</button>
    </p>
  );
}
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

// root 1: the usual root div
createRoot(document.getElementById("root")!).render(<App />);

// root 2: a new element, added to the page with plain JavaScript
const widgetBox = document.createElement("div");
document.body.appendChild(widgetBox);
createRoot(widgetBox).render(<Widget />);
`,
  }}
/>

### React Application Startup Process

When a user opens a CSR app, these steps run once, in order:

1. The browser loads `index.html`. The `root` div is empty.
2. The browser loads and runs `main.tsx` (the **entry point**).
3. `createRoot` connects React to the `root` div.
4. `render(<App />)` asks React to draw `App` and all its children.
5. React creates the DOM nodes and puts them in `root`. The user sees the page.

<figure className="react-fig">
<svg viewBox="0 0 520 120" role="img" aria-labelledby="fig-csr-startup" style={{ width: "100%", maxWidth: 520, height: "auto" }}>
  <title id="fig-csr-startup">React application startup: index.html loads main.tsx, which creates a root and renders App</title>
  <rect x="5" y="30" width="96" height="52" rx="8" style={{ fill: "var(--panel2)", stroke: "var(--faint)", strokeWidth: 1.5 }} />
  <text x="53.0" y="53.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>index.html</text>
  <text x="53.0" y="70.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>empty #root</text>
  <rect x="109" y="30" width="96" height="52" rx="8" style={{ fill: "var(--accent-secondary-soft)", stroke: "var(--accent-secondary)", strokeWidth: 1.5 }} />
  <text x="157.0" y="53.0" textAnchor="middle" style={{ fill: "var(--accent-secondary)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>main.tsx</text>
  <text x="157.0" y="70.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>runs first</text>
  <rect x="213" y="30" width="96" height="52" rx="8" style={{ fill: "var(--info-soft)", stroke: "var(--info)", strokeWidth: 1.5 }} />
  <text x="261.0" y="53.0" textAnchor="middle" style={{ fill: "var(--info)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>createRoot</text>
  <text x="261.0" y="70.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>connect React</text>
  <rect x="317" y="30" width="96" height="52" rx="8" style={{ fill: "var(--accent-soft)", stroke: "var(--accent)", strokeWidth: 1.5 }} />
  <text x="365.0" y="53.0" textAnchor="middle" style={{ fill: "var(--accent)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>render</text>
  <text x="365.0" y="70.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>draw &lt;App /&gt;</text>
  <rect x="421" y="30" width="96" height="52" rx="8" style={{ fill: "var(--ok-soft)", stroke: "var(--ok)", strokeWidth: 1.5 }} />
  <text x="469.0" y="53.0" textAnchor="middle" style={{ fill: "var(--ok)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>Page</text>
  <text x="469.0" y="70.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>is filled</text>
  <rect x="5" y="30" width="96" height="52" rx="8" style={{ fill: "none", stroke: "var(--accent-secondary)", strokeWidth: 3 }} opacity="0">
    <animate attributeName="opacity" values="0;1;0;0" keyTimes="0;0.15;0.4;1" dur="3.5s" begin="0.0s" repeatCount="indefinite" />
  </rect>
  <rect x="109" y="30" width="96" height="52" rx="8" style={{ fill: "none", stroke: "var(--accent-secondary)", strokeWidth: 3 }} opacity="0">
    <animate attributeName="opacity" values="0;1;0;0" keyTimes="0;0.15;0.4;1" dur="3.5s" begin="0.7s" repeatCount="indefinite" />
  </rect>
  <rect x="213" y="30" width="96" height="52" rx="8" style={{ fill: "none", stroke: "var(--accent-secondary)", strokeWidth: 3 }} opacity="0">
    <animate attributeName="opacity" values="0;1;0;0" keyTimes="0;0.15;0.4;1" dur="3.5s" begin="1.4s" repeatCount="indefinite" />
  </rect>
  <rect x="317" y="30" width="96" height="52" rx="8" style={{ fill: "none", stroke: "var(--accent-secondary)", strokeWidth: 3 }} opacity="0">
    <animate attributeName="opacity" values="0;1;0;0" keyTimes="0;0.15;0.4;1" dur="3.5s" begin="2.0999999999999996s" repeatCount="indefinite" />
  </rect>
  <rect x="421" y="30" width="96" height="52" rx="8" style={{ fill: "none", stroke: "var(--accent-secondary)", strokeWidth: 3 }} opacity="0">
    <animate attributeName="opacity" values="0;1;0;0" keyTimes="0;0.15;0.4;1" dur="3.5s" begin="2.8s" repeatCount="indefinite" />
  </rect>
  <text x="260" y="108" textAnchor="middle" style={{ fill: "var(--muted)", fontSize: 12, fontWeight: 400, fontFamily: "var(--sans)" }}>each step happens once, when the page opens</text>
</svg>
<figcaption>This is the start of every Vite React app.</figcaption>
</figure>

The lab prints the `root` div before and after React. Before React, it has 0 children.

      <h1>Book Corner</h1>
      <p>Made in the browser by React.</p>
    </div>
  );
}
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

const rootElement = document.getElementById("root")!;
console.log("1. Before React: root has " + rootElement.childNodes.length + " children");

const root = createRoot(rootElement);
root.render(<App />);
console.log("2. render() was called");

// React draws a moment later. Look again after a short wait.
setTimeout(() => {
  console.log("3. After React: " + rootElement.innerHTML);
}, 50);
`,
  }}
/>

### Browser Rendering Workflow

React makes DOM nodes, but the **browser** turns them into pixels. For a CSR page, the browser does this:

1. **Read the HTML** and build a first, almost empty page.
2. **Download the JavaScript** bundle.
3. **Run the JavaScript.** React creates the DOM nodes.
4. **Build the DOM** tree from those nodes.
5. **Layout:** work out the size and place of each element.
6. **Paint:** draw the pixels on the screen.

Until step 6, the user sees a blank page. That is the main cost of CSR.

<figure className="react-fig">
<svg viewBox="0 0 520 130" role="img" aria-labelledby="fig-csr-browser" style={{ width: "100%", maxWidth: 520, height: "auto" }}>
  <title id="fig-csr-browser">Browser rendering workflow: read HTML, download JavaScript, run it, build the DOM, paint the screen</title>
  <rect x="5" y="30" width="80" height="52" rx="8" style={{ fill: "var(--panel2)", stroke: "var(--faint)", strokeWidth: 1.5 }} />
  <text x="45.0" y="53.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Read HTML</text>
  <text x="45.0" y="70.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>parse</text>
  <rect x="91" y="30" width="80" height="52" rx="8" style={{ fill: "var(--accent-secondary-soft)", stroke: "var(--accent-secondary)", strokeWidth: 1.5 }} />
  <text x="131.0" y="53.0" textAnchor="middle" style={{ fill: "var(--accent-secondary)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Get JS</text>
  <text x="131.0" y="70.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>download</text>
  <rect x="177" y="30" width="80" height="52" rx="8" style={{ fill: "var(--accent-soft)", stroke: "var(--accent)", strokeWidth: 1.5 }} />
  <text x="217.0" y="53.0" textAnchor="middle" style={{ fill: "var(--accent)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Run JS</text>
  <text x="217.0" y="70.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>React works</text>
  <rect x="263" y="30" width="80" height="52" rx="8" style={{ fill: "var(--info-soft)", stroke: "var(--info)", strokeWidth: 1.5 }} />
  <text x="303.0" y="53.0" textAnchor="middle" style={{ fill: "var(--info)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Build DOM</text>
  <text x="303.0" y="70.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>nodes</text>
  <rect x="349" y="30" width="80" height="52" rx="8" style={{ fill: "var(--info-soft)", stroke: "var(--info)", strokeWidth: 1.5 }} />
  <text x="389.0" y="53.0" textAnchor="middle" style={{ fill: "var(--info)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Layout</text>
  <text x="389.0" y="70.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>sizes</text>
  <rect x="435" y="30" width="80" height="52" rx="8" style={{ fill: "var(--ok-soft)", stroke: "var(--ok)", strokeWidth: 1.5 }} />
  <text x="475.0" y="53.0" textAnchor="middle" style={{ fill: "var(--ok)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Paint</text>
  <text x="475.0" y="70.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>pixels</text>
  <rect x="5" y="96" width="0" height="10" rx="5" style={{ fill: "var(--accent)" }}>
    <animate attributeName="width" values="0;510;510" keyTimes="0;0.85;1" dur="5s" repeatCount="indefinite" />
  </rect>
</svg>
<figcaption>The screen stays blank until the paint step at the end.</figcaption>
</figure>

## React Rendering Fundamentals

### Initial Render

The **initial render** is the first time React draws your app. It happens once, when `root.render(<App />)` runs. React calls every component, and creates **all** the DOM nodes from nothing.

### Re-rendering

A **re-render** means React calls a component function again, to get its new TSX. It happens after the initial render, when something changes.

A re-render does **not** mean the whole page is rebuilt. React compares the new result with the old one, and changes only the parts that differ.

The lab below shows re-renders. Each box flashes when its component renders. Click the buttons and watch which boxes flash.

      <b>{name}</b> rendered {renders.current} time(s)
      {children}
    </div>
  );
}

function Child({ label }: { label: string }) {
  return <RenderBox name={"Child: " + label} />;
}

  const [count, setCount] = useState(0);
  const [title, setTitle] = useState("Books");

  return (
      <p>count = {count}, title = {title}</p>
      <button onClick={() => setCount(count + 1)}>Change count</button>{" "}
      <button onClick={() => setTitle(title === "Books" ? "Pens" : "Books")}>Change title</button>
  );
}
`,
    "flash.css": `.box {
  border: 2px solid #2b59c3;
  border-radius: 8px;
  padding: 8px;
  margin: 6px 0;
  font-family: sans-serif;
  animation-duration: 0.8s;
}
@keyframes flashA { from { background: #ffe58a; } to { background: transparent; } }
@keyframes flashB { from { background: #ffe58a; } to { background: transparent; } }
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

Notice: "Child: Always the same" also flashes. Its props did not change, but its parent (App) re-rendered, so it re-renders too.

### Render Cycle

Every update goes through the same four steps, called the **render cycle**:

1. **Trigger.** Something asks for an update: the first load, or a state change.
2. **Render.** React calls the components and builds the new Virtual DOM. The screen does not change yet.
3. **Commit.** React applies the changes to the real DOM. Now the user sees them.
4. **Effects.** React runs your `useEffect` code.

<figure className="react-fig">
<svg viewBox="0 0 520 160" role="img" aria-labelledby="fig-csr-cycle" style={{ width: "100%", maxWidth: 520, height: "auto" }}>
  <title id="fig-csr-cycle">The render cycle: a trigger starts a render, React commits changes to the DOM, then effects run</title>
  <rect x="10" y="55" width="110" height="50" rx="8" style={{ fill: "var(--accent-soft)", stroke: "var(--accent)", strokeWidth: 1.5 }} />
  <text x="65.0" y="77.0" textAnchor="middle" style={{ fill: "var(--accent)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>1. Trigger</text>
  <text x="65.0" y="94.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>state or props</text>
  <path d="M120 80 L145 80" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M145 80 L136.7 83.5 L136.7 76.5 Z" style={{ fill: "var(--faint)" }} />
  <rect x="150" y="55" width="110" height="50" rx="8" style={{ fill: "var(--accent-secondary-soft)", stroke: "var(--accent-secondary)", strokeWidth: 1.5 }} />
  <text x="205.0" y="77.0" textAnchor="middle" style={{ fill: "var(--accent-secondary)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>2. Render</text>
  <text x="205.0" y="94.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>call components</text>
  <path d="M260 80 L285 80" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M285 80 L276.7 83.5 L276.7 76.5 Z" style={{ fill: "var(--faint)" }} />
  <rect x="290" y="55" width="110" height="50" rx="8" style={{ fill: "var(--ok-soft)", stroke: "var(--ok)", strokeWidth: 1.5 }} />
  <text x="345.0" y="77.0" textAnchor="middle" style={{ fill: "var(--ok)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>3. Commit</text>
  <text x="345.0" y="94.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>update the DOM</text>
  <path d="M400 80 L420 80" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M420 80 L411.7 83.5 L411.7 76.5 Z" style={{ fill: "var(--faint)" }} />
  <rect x="425" y="55" width="90" height="50" rx="8" style={{ fill: "var(--info-soft)", stroke: "var(--info)", strokeWidth: 1.5 }} />
  <text x="470.0" y="77.0" textAnchor="middle" style={{ fill: "var(--info)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>4. Effects</text>
  <text x="470.0" y="94.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>useEffect</text>
  <path d="M470 105 Q470 150 260 150 Q65 150 65 109" style={{ fill: "none", stroke: "var(--faint)", strokeWidth: 2, strokeDasharray: "5 4" }} />
  <text x="260" y="142" textAnchor="middle" style={{ fill: "var(--muted)", fontSize: 11, fontWeight: 400, fontFamily: "var(--sans)" }}>a new setState starts the cycle again</text>
  <circle r="6" style={{ fill: "var(--accent-secondary)" }}>
    <animateMotion dur="4s" repeatCount="indefinite" path="M120 80 L145 80 M260 80 L285 80 M400 80 L420 80 M470 105 Q470 150 260 150 Q65 150 65 109" />
  </circle>
</svg>
<figcaption>Render is only calculating. Commit is when the screen changes.</figcaption>
</figure>

Step through the cycle yourself. Click **Next step** to move through the four steps for one button click.

      <svg viewBox="0 0 440 80" style={{ width: "100%" }}>
        {steps.map((s, i) => (
          <g key={s.name}>
            <rect
              x={i * 110 + 4}
              y={14}
              width={96}
              height={44}
              rx={10}
              fill={i === step ? s.color : "#f1f3f7"}
              stroke={s.color}
              strokeWidth={2}
            />
            <text x={i * 110 + 52} y={41} textAnchor="middle" fontSize={14} fontWeight="bold" fill={i === step ? "white" : s.color}>
              {i + 1}. {s.name}
            </text>
          </g>
        ))}
      </svg>
      <p style={{ minHeight: 48 }}>{steps[step].text}</p>
      <button onClick={() => setStep((step + 1) % steps.length)}>
        {step === steps.length - 1 ? "Start again" : "Next step"}
      </button>
    </div>
  );
}
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

Easy Way to Remember: trigger, render, commit, effects. Render only calculates. Commit is when the screen changes.

The real order is in the console of this next lab. Children render after their parent, but child effects run **before** the parent's effect.

      <button onClick={() => { console.log("--- trigger: setCount ---"); setCount(count + 1); }}>
        Update
      </button>
    </div>
  );
}
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

## Virtual DOM and Updates

### Virtual DOM

The **DOM** is the browser's tree of real page elements. Changing it is slow, because the browser must do layout and paint again.

The **Virtual DOM** is a light copy of the page made of plain JavaScript objects. `<h1 className="title">Hello</h1>` becomes an object like:

```ts
{ type: "h1", props: { className: "title", children: "Hello" } }
```

Making and comparing these objects is very fast. React changes the Virtual DOM first, and touches the real DOM only where needed.

      <h2>Wings of Fire</h2>
      <p>₹350</p>
    </div>
  );
  console.log("card children: " + card.props.children.length);
  console.log("first child type: " + card.props.children[0].type);

  return card;
}
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

### Reconciliation

**Reconciliation** is how React updates the page. On every re-render:

1. React builds a **new** Virtual DOM tree.
2. It compares it with the **old** tree.
3. It changes only the real DOM nodes that differ.

The lab watches the real DOM with a `MutationObserver`, a browser tool that reports every DOM change. Click the button: only one text changes. The heading and list are never touched.

      <h2>Book Corner</h2>
      <ul>
        <li>The Alchemist</li>
        <li>Wings of Fire</li>
      </ul>
      <button onClick={() => setCount(count + 1)}>Cart: {count}</button>
    </div>
  );
}
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

### Diffing Algorithm

**Diffing** is the "compare" step. React uses two simple rules, so it stays fast:

- **Different type, new tree.** If an element changes type (`<div>` becomes `<section>`), React removes the old part and builds a new one. State inside it is lost.
- **Same type, update.** If the type is the same, React keeps the element and changes only the props that differ.

In lists, React uses the **key** to match old items with new ones. So keys must be stable, like an `id`.

<figure className="react-fig">
<svg viewBox="0 0 520 190" role="img" aria-labelledby="fig-csr-diff" style={{ width: "100%", maxWidth: 520, height: "auto" }}>
  <title id="fig-csr-diff">Diffing: React compares the old and new Virtual DOM trees and updates only the node that changed</title>
  <text x="130" y="18" textAnchor="middle" style={{ fill: "var(--muted)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Old Virtual DOM</text>
  <text x="390" y="18" textAnchor="middle" style={{ fill: "var(--muted)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>New Virtual DOM</text>
  <rect x="85" y="30" width="90" height="30" rx="8" style={{ fill: "var(--panel2)", stroke: "var(--faint)", strokeWidth: 1.5 }} />
  <text x="130.0" y="50.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>div</text>
  <rect x="20" y="100" width="90" height="30" rx="8" style={{ fill: "var(--panel2)", stroke: "var(--faint)", strokeWidth: 1.5 }} />
  <text x="65.0" y="120.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>h1</text>
  <rect x="150" y="100" width="90" height="30" rx="8" style={{ fill: "var(--panel2)", stroke: "var(--faint)", strokeWidth: 1.5 }} />
  <text x="195.0" y="120.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>p: 0</text>
  <path d="M115 60 L70 96" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M70 96 L74.3 88.1 L78.7 93.6 Z" style={{ fill: "var(--faint)" }} />
  <path d="M145 60 L190 96" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M190 96 L181.3 93.6 L185.7 88.1 Z" style={{ fill: "var(--faint)" }} />
  <rect x="345" y="30" width="90" height="30" rx="8" style={{ fill: "var(--panel2)", stroke: "var(--faint)", strokeWidth: 1.5 }} />
  <text x="390.0" y="50.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>div</text>
  <rect x="280" y="100" width="90" height="30" rx="8" style={{ fill: "var(--panel2)", stroke: "var(--faint)", strokeWidth: 1.5 }} />
  <text x="325.0" y="120.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>h1</text>
  <rect x="410" y="100" width="90" height="30" rx="8" style={{ fill: "var(--bad-soft)", stroke: "var(--bad)", strokeWidth: 1.5 }} />
  <text x="455.0" y="120.0" textAnchor="middle" style={{ fill: "var(--bad)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>p: 1</text>
  <path d="M375 60 L330 96" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M330 96 L334.3 88.1 L338.7 93.6 Z" style={{ fill: "var(--faint)" }} />
  <path d="M405 60 L450 96" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M450 96 L441.3 93.6 L445.7 88.1 Z" style={{ fill: "var(--faint)" }} />
  <rect x="410" y="100" width="90" height="30" rx="8" style={{ fill: "none", stroke: "var(--bad)", strokeWidth: 3 }} opacity="0">
    <animate attributeName="opacity" values="0;1;0;0" keyTimes="0;0.15;0.4;1" dur="3s" begin="0s" repeatCount="indefinite" />
  </rect>
  <text x="260" y="165" textAnchor="middle" style={{ fill: "var(--bad)", fontSize: 12, fontWeight: 700, fontFamily: "var(--sans)" }}>only &lt;p&gt; changed, so only &lt;p&gt; is updated on the page</text>
</svg>
<figcaption>The h1 and the div stay as they are. That is why React is fast.</figcaption>
</figure>

Try both rules:

- Click **+1** a few times, then **Switch wrapper**. The counter goes back to 0, because `div` became `section`.
- Type in the first box of each list, then click **Add at top**. With `key={index}` the text stays in the wrong row. With `key={book.id}` it moves with its book.

      {useSection ? (
        <section><Counter /></section>
      ) : (
        <div><Counter /></div>
      )}
      <button onClick={() => setUseSection(!useSection)}>
        Switch wrapper (now {useSection ? "section" : "div"})
      </button>
      <hr />
    </div>
  );
}
`,
    "BookLists.tsx": `import { useState } from "react";

interface Book {
  id: number;
  title: string;
}

  const [books, setBooks] = useState<Book[]>([
    { id: 1, title: "The Alchemist" },
    { id: 2, title: "Atomic Habits" },
  ]);

  function addAtTop() {
    setBooks([{ id: Date.now(), title: "Wings of Fire" }, ...books]);
  }

  return (
    <div style={{ display: "flex", gap: 20 }}>
      <ul>
        <b>key = index</b>
        {books.map((b, index) => (
          <li key={index}>{b.title} <input size={6} /></li>
        ))}
      </ul>
      <ul>
        <b>key = id</b>
        {books.map((b) => (
          <li key={b.id}>{b.title} <input size={6} /></li>
        ))}
      </ul>
      <button onClick={addAtTop}>Add at top</button>
    </div>
  );
}
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

<figure className="react-fig">
<svg viewBox="0 0 520 130" role="img" aria-labelledby="fig-csr-keys" style={{ width: "100%", maxWidth: 520, height: "auto" }}>
  <title id="fig-csr-keys">With keys, React knows which item is which when the list changes</title>
  <text x="130" y="18" textAnchor="middle" style={{ fill: "var(--bad)", fontSize: 12, fontWeight: 700, fontFamily: "var(--sans)" }}>key = index: React gets confused</text>
  <text x="390" y="18" textAnchor="middle" style={{ fill: "var(--ok)", fontSize: 12, fontWeight: 700, fontFamily: "var(--sans)" }}>key = id: React matches items</text>
  <rect x="40" y="30" width="180" height="26" rx="8" style={{ fill: "var(--bad-soft)", stroke: "var(--bad)", strokeWidth: 1.5 }} />
  <text x="130.0" y="48.0" textAnchor="middle" style={{ fill: "var(--bad)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>0: Wings of Fire (new)</text>
  <rect x="40" y="62" width="180" height="26" rx="8" style={{ fill: "var(--bad-soft)", stroke: "var(--bad)", strokeWidth: 1.5 }} />
  <text x="130.0" y="80.0" textAnchor="middle" style={{ fill: "var(--bad)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>1: The Alchemist</text>
  <rect x="40" y="94" width="180" height="26" rx="8" style={{ fill: "var(--bad-soft)", stroke: "var(--bad)", strokeWidth: 1.5 }} />
  <text x="130.0" y="112.0" textAnchor="middle" style={{ fill: "var(--bad)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>2: Atomic Habits</text>
  <rect x="300" y="30" width="180" height="26" rx="8" style={{ fill: "var(--ok-soft)", stroke: "var(--ok)", strokeWidth: 1.5 }} />
  <text x="390.0" y="48.0" textAnchor="middle" style={{ fill: "var(--ok)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>id 4: Wings of Fire</text>
  <rect x="300" y="62" width="180" height="26" rx="8" style={{ fill: "var(--panel2)", stroke: "var(--faint)", strokeWidth: 1.5 }} />
  <text x="390.0" y="80.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>id 1: The Alchemist</text>
  <rect x="300" y="94" width="180" height="26" rx="8" style={{ fill: "var(--panel2)", stroke: "var(--faint)", strokeWidth: 1.5 }} />
  <text x="390.0" y="112.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>id 2: Atomic Habits</text>
  <rect x="300" y="30" width="180" height="26" rx="8" style={{ fill: "none", stroke: "var(--ok)", strokeWidth: 3 }} opacity="0">
    <animate attributeName="opacity" values="0;1;0;0" keyTimes="0;0.15;0.4;1" dur="3s" begin="0s" repeatCount="indefinite" />
  </rect>
</svg>
<figcaption>Left: every row looks changed. Right: React sees one new row.</figcaption>
</figure>

## Rendering Triggers

A component renders again only when something tells React to. There are three common triggers.

<figure className="react-fig">
<svg viewBox="0 0 520 130" role="img" aria-labelledby="fig-csr-triggers" style={{ width: "100%", maxWidth: 520, height: "auto" }}>
  <title id="fig-csr-triggers">Three things start a re-render: a state change, new props, and a user action that sets state</title>
  <rect x="10" y="15" width="150" height="46" rx="8" style={{ fill: "var(--accent-soft)", stroke: "var(--accent)", strokeWidth: 1.5 }} />
  <text x="85.0" y="35.0" textAnchor="middle" style={{ fill: "var(--accent)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>State change</text>
  <text x="85.0" y="52.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>setCount(1)</text>
  <rect x="185" y="15" width="150" height="46" rx="8" style={{ fill: "var(--accent-secondary-soft)", stroke: "var(--accent-secondary)", strokeWidth: 1.5 }} />
  <text x="260.0" y="35.0" textAnchor="middle" style={{ fill: "var(--accent-secondary)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>New props</text>
  <text x="260.0" y="52.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>price={399}</text>
  <rect x="360" y="15" width="150" height="46" rx="8" style={{ fill: "var(--ok-soft)", stroke: "var(--ok)", strokeWidth: 1.5 }} />
  <text x="435.0" y="35.0" textAnchor="middle" style={{ fill: "var(--ok)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>User action</text>
  <text x="435.0" y="52.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>click, type</text>
  <path d="M85 61 L240 92" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M240 92 L231.2 93.8 L232.6 86.9 Z" style={{ fill: "var(--faint)" }} />
  <path d="M260 61 L260 90" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M260 90 L256.5 81.7 L263.5 81.7 Z" style={{ fill: "var(--faint)" }} />
  <path d="M435 61 L280 92" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M280 92 L287.4 86.9 L288.8 93.8 Z" style={{ fill: "var(--faint)" }} />
  <rect x="185" y="94" width="150" height="32" rx="8" style={{ fill: "var(--info-soft)", stroke: "var(--info)", strokeWidth: 1.5 }} />
  <text x="260.0" y="115.0" textAnchor="middle" style={{ fill: "var(--info)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Re-render</text>
  <rect x="185" y="94" width="150" height="32" rx="8" style={{ fill: "none", stroke: "var(--info)", strokeWidth: 3 }} opacity="0">
    <animate attributeName="opacity" values="0;1;0;0" keyTimes="0;0.15;0.4;1" dur="2.5s" begin="0s" repeatCount="indefinite" />
  </rect>
</svg>
<figcaption>A user action usually calls a setter, so it becomes a state change.</figcaption>
</figure>

### State-Driven Rendering

When a component's **state** changes through its setter, React re-renders it. A normal variable does **not** do this: React never knows it changed.

### Props-Driven Rendering

When a parent re-renders and passes new **props**, the child re-renders and shows the new values. The child needs no state of its own.

### User Interaction and UI Updates

Users do not change state directly. They click and type. Your **event handler** calls a setter, and that starts the render cycle:

**click → handler → setState → re-render → screen updates**

The lab shows all three. `App` owns the state, `PriceTag` gets props, and the buttons and slider are the user interaction. The last button changes a normal variable, and nothing happens.

      Atomic Habits: <s>₹{price}</s> <b>₹{final}</b>
    </p>
  );
}

  const [discount, setDiscount] = useState(0);
  let normal = 0;

  return (
    <div>
      <input
        type="range"
        min={0}
        max={50}
        step={5}
        value={discount}
        onChange={(e) => setDiscount(Number(e.target.value))}
      />{" "}
      {discount}% off
      <p>
        <button onClick={() => setDiscount(0)}>Reset (state)</button>{" "}
        <button onClick={() => { normal = normal + 10; console.log("normal is " + normal + ", but nothing re-renders"); }}>
          Add 10% (normal variable)
        </button>
      </p>
    </div>
  );
}
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

Setting state to the same value it already has (for example `setDiscount(0)` when it is 0) usually does not re-render. React sees nothing changed and stops.

## Advantages and Limitations of CSR

<div className="tscroll">

| Advantages | Limitations |
|---|---|
| Instant page changes after loading | Blank page until the JavaScript loads and runs |
| Rich, app-like interaction | Slow first view on weak phones and networks |
| Hosted as simple static files | Some search engines and link previews see an empty page |
| Little work for the server | Large bundles make the wait longer |

</div>

CSR is a great fit for apps used after login, like dashboards, admin panels and tools. For public pages that must load fast and appear in search, look at SSR in the next lesson.

## Capstone Project: Product Catalog

### What is Capstone Project

A **capstone project** is one small but complete app that uses everything from the lesson. Here you build a **Product Catalog** for Book Corner, rendered fully in the browser (CSR). Users can search products, filter by category, and add items to a cart.

<figure className="react-fig">
<svg viewBox="0 0 520 170" role="img" aria-labelledby="fig-csr-catalog" style={{ width: "100%", maxWidth: 520, height: "auto" }}>
  <title id="fig-csr-catalog">The Product Catalog component tree: App owns the state and passes it down</title>
  <rect x="185" y="10" width="150" height="46" rx="8" style={{ fill: "var(--accent-soft)", stroke: "var(--accent)", strokeWidth: 1.5 }} />
  <text x="260.0" y="30.0" textAnchor="middle" style={{ fill: "var(--accent)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>App</text>
  <text x="260.0" y="47.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>query, category, cart</text>
  <rect x="5" y="110" width="120" height="44" rx="8" style={{ fill: "var(--panel2)", stroke: "var(--faint)", strokeWidth: 1.5 }} />
  <text x="65.0" y="129.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>SearchBar</text>
  <text x="65.0" y="146.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>input</text>
  <rect x="135" y="110" width="120" height="44" rx="8" style={{ fill: "var(--panel2)", stroke: "var(--faint)", strokeWidth: 1.5 }} />
  <text x="195.0" y="129.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>CategoryFilter</text>
  <text x="195.0" y="146.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>buttons</text>
  <rect x="265" y="110" width="120" height="44" rx="8" style={{ fill: "var(--panel2)", stroke: "var(--faint)", strokeWidth: 1.5 }} />
  <text x="325.0" y="129.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>ProductList</text>
  <text x="325.0" y="146.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>grid</text>
  <rect x="395" y="110" width="120" height="44" rx="8" style={{ fill: "var(--accent-secondary-soft)", stroke: "var(--accent-secondary)", strokeWidth: 1.5 }} />
  <text x="455.0" y="129.0" textAnchor="middle" style={{ fill: "var(--accent-secondary)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>ProductCard</text>
  <text x="455.0" y="146.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>× many</text>
  <path d="M220 56 L65 106" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M65 106 L71.8 100.1 L74.0 106.8 Z" style={{ fill: "var(--faint)" }} />
  <path d="M245 56 L195 106" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M195 106 L198.4 97.7 L203.3 102.6 Z" style={{ fill: "var(--faint)" }} />
  <path d="M275 56 L325 106" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M325 106 L316.7 102.6 L321.6 97.7 Z" style={{ fill: "var(--faint)" }} />
  <path d="M385 132 L391 132" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M391 132 L382.7 135.5 L382.7 128.5 Z" style={{ fill: "var(--faint)" }} />
  <circle r="6" style={{ fill: "var(--accent)" }}>
    <animateMotion dur="2.2s" repeatCount="indefinite" path="M220 56 L65 106" />
  </circle>
  <circle r="6" style={{ fill: "var(--ok)" }}>
    <animateMotion dur="2.2s" repeatCount="indefinite" path="M65 106 L220 56" />
  </circle>
</svg>
<figcaption>Data goes down as props. Changes come up through onChange and onAdd.</figcaption>
</figure>

### Build reusable product components

Start with `types.ts` (the `Product` shape), `data.ts` (8 sample products) and one reusable component, `ProductCard`. It gets a `product` and an `onAdd` function as props. Out-of-stock items show a message and a disabled button.

      <div className="grid">
        <ProductCard product={products[0]} onAdd={(id) => console.log("Add product " + id)} />
        <ProductCard product={products[2]} onAdd={(id) => console.log("Add product " + id)} />
      </div>
    </div>
  );
}
`,
    "ProductCard.tsx": `import { memo } from "react";

interface ProductCardProps {
  product: Product;
  onAdd: (id: number) => void;
}

function ProductCard({ product, onAdd }: ProductCardProps) {
  return (
    <div className="card">
      <span className="name">{product.name}</span>
      <span className="meta">{product.category} · ★ {product.rating}</span>
      <span className="price">₹{product.price}</span>
      {product.inStock ? (
        <button className="add" onClick={() => onAdd(product.id)}>Add to cart</button>
      ) : (
        <>
          <span className="out">Out of stock</span>
          <button className="add" disabled>Add to cart</button>
        </>
      )}
    </div>
  );
}

`,
    "types.ts": `export type Category = "Books" | "Stationery" | "Bags";

  id: number;
  name: string;
  category: Category;
  price: number;
  rating: number;
  inStock: boolean;
}
`,
    "data.ts": `import type { Product } from "./types";

  { id: 1, name: "The Alchemist", category: "Books", price: 299, rating: 4.6, inStock: true },
  { id: 2, name: "Wings of Fire", category: "Books", price: 350, rating: 4.8, inStock: true },
  { id: 3, name: "Atomic Habits", category: "Books", price: 499, rating: 4.7, inStock: false },
  { id: 4, name: "Gel Pen Set", category: "Stationery", price: 120, rating: 4.2, inStock: true },
  { id: 5, name: "A5 Notebook", category: "Stationery", price: 180, rating: 4.4, inStock: true },
  { id: 6, name: "Highlighter Pack", category: "Stationery", price: 150, rating: 4.0, inStock: true },
  { id: 7, name: "Canvas Tote Bag", category: "Bags", price: 399, rating: 4.3, inStock: true },
  { id: 8, name: "Laptop Backpack", category: "Bags", price: 1299, rating: 4.5, inStock: false },
];
`,
    "styles.css": `.shop { max-width: 640px; font-family: sans-serif; }
.shop h1 { margin: 0 0 8px; color: #2b59c3; }
.toolbar { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 12px; }
.search { flex: 1; min-width: 160px; padding: 7px 10px; border: 1px solid #bbb; border-radius: 8px; }
.cats { display: flex; gap: 6px; }
.cats button { padding: 5px 10px; border: 1px solid #bbb; border-radius: 999px; background: white; cursor: pointer; }
.cats button.active { background: #2b59c3; border-color: #2b59c3; color: white; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 10px; }
.card { border: 1px solid #ddd; border-radius: 10px; padding: 10px; display: grid; gap: 4px; background: white; }
.card .name { font-weight: bold; }
.card .meta { color: #666; font-size: 13px; }
.card .price { font-size: 18px; }
.card .out { color: #b42318; font-size: 13px; }
.card button { padding: 5px; border-radius: 6px; border: 1px solid #2b59c3; background: #2b59c3; color: white; cursor: pointer; }
.card button:disabled { background: #ccc; border-color: #ccc; cursor: default; }
.cart { background: #fff3cd; padding: 6px 10px; border-radius: 8px; }
.empty { color: #666; }
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

### Display products using list rendering

`ProductList` uses `.map` to turn the array into cards. Each card gets `key={p.id}`, so React can match cards when the list changes. When the list is empty, it shows a friendly message instead.

      <h1>Product Catalog</h1>
      <ProductList products={products} onAdd={(id) => console.log("Add product " + id)} />
    </div>
  );
}
`,
    "ProductList.tsx": `import type { Product } from "./types";

interface ProductListProps {
  products: Product[];
  onAdd: (id: number) => void;
}

  if (products.length === 0) return <p className="empty">No products match your search.</p>;
  return (
    <div className="grid">
      {products.map((p) => (
      ))}
    </div>
  );
}
`,
    "ProductCard.tsx": `import { memo } from "react";

interface ProductCardProps {
  product: Product;
  onAdd: (id: number) => void;
}

function ProductCard({ product, onAdd }: ProductCardProps) {
  return (
    <div className="card">
      <span className="name">{product.name}</span>
      <span className="meta">{product.category} · ★ {product.rating}</span>
      <span className="price">₹{product.price}</span>
      {product.inStock ? (
        <button className="add" onClick={() => onAdd(product.id)}>Add to cart</button>
      ) : (
        <>
          <span className="out">Out of stock</span>
          <button className="add" disabled>Add to cart</button>
        </>
      )}
    </div>
  );
}

`,
    "types.ts": `export type Category = "Books" | "Stationery" | "Bags";

  id: number;
  name: string;
  category: Category;
  price: number;
  rating: number;
  inStock: boolean;
}
`,
    "data.ts": `import type { Product } from "./types";

  { id: 1, name: "The Alchemist", category: "Books", price: 299, rating: 4.6, inStock: true },
  { id: 2, name: "Wings of Fire", category: "Books", price: 350, rating: 4.8, inStock: true },
  { id: 3, name: "Atomic Habits", category: "Books", price: 499, rating: 4.7, inStock: false },
  { id: 4, name: "Gel Pen Set", category: "Stationery", price: 120, rating: 4.2, inStock: true },
  { id: 5, name: "A5 Notebook", category: "Stationery", price: 180, rating: 4.4, inStock: true },
  { id: 6, name: "Highlighter Pack", category: "Stationery", price: 150, rating: 4.0, inStock: true },
  { id: 7, name: "Canvas Tote Bag", category: "Bags", price: 399, rating: 4.3, inStock: true },
  { id: 8, name: "Laptop Backpack", category: "Bags", price: 1299, rating: 4.5, inStock: false },
];
`,
    "styles.css": `.shop { max-width: 640px; font-family: sans-serif; }
.shop h1 { margin: 0 0 8px; color: #2b59c3; }
.toolbar { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 12px; }
.search { flex: 1; min-width: 160px; padding: 7px 10px; border: 1px solid #bbb; border-radius: 8px; }
.cats { display: flex; gap: 6px; }
.cats button { padding: 5px 10px; border: 1px solid #bbb; border-radius: 999px; background: white; cursor: pointer; }
.cats button.active { background: #2b59c3; border-color: #2b59c3; color: white; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 10px; }
.card { border: 1px solid #ddd; border-radius: 10px; padding: 10px; display: grid; gap: 4px; background: white; }
.card .name { font-weight: bold; }
.card .meta { color: #666; font-size: 13px; }
.card .price { font-size: 18px; }
.card .out { color: #b42318; font-size: 13px; }
.card button { padding: 5px; border-radius: 6px; border: 1px solid #2b59c3; background: #2b59c3; color: white; cursor: pointer; }
.card button:disabled { background: #ccc; border-color: #ccc; cursor: default; }
.cart { background: #fff3cd; padding: 6px 10px; border-radius: 8px; }
.empty { color: #666; }
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

### Implement search functionality

`SearchBar` is a controlled input. `App` keeps the text in `query` state. The visible list is **worked out** from `query` on every render. We do not store a second, filtered copy in state:

```tsx
const visible = products.filter((p) => p.name.toLowerCase().includes(query.trim().toLowerCase()));
```

`toLowerCase()` on both sides makes the search ignore capital letters.

      <h1>Product Catalog</h1>
      <div className="toolbar">
      </div>
      <ProductList products={visible} onAdd={(id) => console.log("Add product " + id)} />
    </div>
  );
}
`,
    "SearchBar.tsx": `interface SearchBarProps {
  value: string;
  onChange: (value: string) => void;
}

  return (
    <input
      className="search"
      value={value}
      onChange={(e) => onChange(e.target.value)}
      placeholder="Search products..."
    />
  );
}
`,
    "ProductList.tsx": `import type { Product } from "./types";

interface ProductListProps {
  products: Product[];
  onAdd: (id: number) => void;
}

  if (products.length === 0) return <p className="empty">No products match your search.</p>;
  return (
    <div className="grid">
      {products.map((p) => (
      ))}
    </div>
  );
}
`,
    "ProductCard.tsx": `import { memo } from "react";

interface ProductCardProps {
  product: Product;
  onAdd: (id: number) => void;
}

function ProductCard({ product, onAdd }: ProductCardProps) {
  return (
    <div className="card">
      <span className="name">{product.name}</span>
      <span className="meta">{product.category} · ★ {product.rating}</span>
      <span className="price">₹{product.price}</span>
      {product.inStock ? (
        <button className="add" onClick={() => onAdd(product.id)}>Add to cart</button>
      ) : (
        <>
          <span className="out">Out of stock</span>
          <button className="add" disabled>Add to cart</button>
        </>
      )}
    </div>
  );
}

`,
    "types.ts": `export type Category = "Books" | "Stationery" | "Bags";

  id: number;
  name: string;
  category: Category;
  price: number;
  rating: number;
  inStock: boolean;
}
`,
    "data.ts": `import type { Product } from "./types";

  { id: 1, name: "The Alchemist", category: "Books", price: 299, rating: 4.6, inStock: true },
  { id: 2, name: "Wings of Fire", category: "Books", price: 350, rating: 4.8, inStock: true },
  { id: 3, name: "Atomic Habits", category: "Books", price: 499, rating: 4.7, inStock: false },
  { id: 4, name: "Gel Pen Set", category: "Stationery", price: 120, rating: 4.2, inStock: true },
  { id: 5, name: "A5 Notebook", category: "Stationery", price: 180, rating: 4.4, inStock: true },
  { id: 6, name: "Highlighter Pack", category: "Stationery", price: 150, rating: 4.0, inStock: true },
  { id: 7, name: "Canvas Tote Bag", category: "Bags", price: 399, rating: 4.3, inStock: true },
  { id: 8, name: "Laptop Backpack", category: "Bags", price: 1299, rating: 4.5, inStock: false },
];
`,
    "styles.css": `.shop { max-width: 640px; font-family: sans-serif; }
.shop h1 { margin: 0 0 8px; color: #2b59c3; }
.toolbar { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 12px; }
.search { flex: 1; min-width: 160px; padding: 7px 10px; border: 1px solid #bbb; border-radius: 8px; }
.cats { display: flex; gap: 6px; }
.cats button { padding: 5px 10px; border: 1px solid #bbb; border-radius: 999px; background: white; cursor: pointer; }
.cats button.active { background: #2b59c3; border-color: #2b59c3; color: white; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 10px; }
.card { border: 1px solid #ddd; border-radius: 10px; padding: 10px; display: grid; gap: 4px; background: white; }
.card .name { font-weight: bold; }
.card .meta { color: #666; font-size: 13px; }
.card .price { font-size: 18px; }
.card .out { color: #b42318; font-size: 13px; }
.card button { padding: 5px; border-radius: 6px; border: 1px solid #2b59c3; background: #2b59c3; color: white; cursor: pointer; }
.card button:disabled { background: #ccc; border-color: #ccc; cursor: default; }
.cart { background: #fff3cd; padding: 6px 10px; border-radius: 8px; }
.empty { color: #666; }
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

### Implement category filtering

`CategoryFilter` shows one button per category. The active one gets the class `active`. `App` keeps `category` in state, and the filter now checks **both** the category and the search text:

```tsx
(category === "All" || p.category === category) && p.name.toLowerCase().includes(text)
```

### Manage product state using Hooks

The finished app uses the hooks from earlier lessons:

- `useState` for `query`, `category` and `cart` (a list of product ids).
- `useMemo` so the filtered list is worked out again only when `query` or `category` change.
- `useCallback` so `addToCart` stays the same function, and `memo(ProductCard)` can skip cards that did not change.
- `useEffect` for a side effect: it shows the cart size in the browser tab title.

### Handle user interactions and UI updates

Every interaction follows the same path: **event → setter → re-render**.

- Typing calls `setQuery`, so the list updates as you type.
- A category button calls `setCategory`.
- **Add to cart** calls `setCart((c) => [...c, id])`, so the cart count and total update.

Here is the complete Product Catalog. Open each tab to see how the parts connect.

        (category === "All" || p.category === category) &&
        p.name.toLowerCase().includes(text),
    );
  }, [query, category]);

  // The same function on every render, so memo(ProductCard) can skip work
  const addToCart = useCallback((id: number) => {
    setCart((c) => [...c, id]);
  }, []);

  const total = cart.reduce((sum, id) => sum + (products.find((p) => p.id === id)?.price ?? 0), 0);

  // A side effect: show the cart size in the browser tab
  useEffect(() => {
    document.title = "(" + cart.length + ") Product Catalog";
  }, [cart.length]);

  return (
    <div className="shop">
      <h1>Product Catalog</h1>
      <div className="toolbar">
        <span className="cart">🛒 <b className="cart-count">{cart.length}</b> · ₹{total}</span>
      </div>
      <div className="toolbar">
        <span className="empty">{visible.length} of {products.length} products</span>
      </div>
    </div>
  );
}
`,
    "ProductList.tsx": `import type { Product } from "./types";

interface ProductListProps {
  products: Product[];
  onAdd: (id: number) => void;
}

  if (products.length === 0) return <p className="empty">No products match your search.</p>;
  return (
    <div className="grid">
      {products.map((p) => (
      ))}
    </div>
  );
}
`,
    "ProductCard.tsx": `import { memo } from "react";

interface ProductCardProps {
  product: Product;
  onAdd: (id: number) => void;
}

function ProductCard({ product, onAdd }: ProductCardProps) {
  return (
    <div className="card">
      <span className="name">{product.name}</span>
      <span className="meta">{product.category} · ★ {product.rating}</span>
      <span className="price">₹{product.price}</span>
      {product.inStock ? (
        <button className="add" onClick={() => onAdd(product.id)}>Add to cart</button>
      ) : (
        <>
          <span className="out">Out of stock</span>
          <button className="add" disabled>Add to cart</button>
        </>
      )}
    </div>
  );
}

`,
    "SearchBar.tsx": `interface SearchBarProps {
  value: string;
  onChange: (value: string) => void;
}

  return (
    <input
      className="search"
      value={value}
      onChange={(e) => onChange(e.target.value)}
      placeholder="Search products..."
    />
  );
}
`,
    "CategoryFilter.tsx": `import type { Category } from "./types";

const CATEGORIES: (Category | "All")[] = ["All", "Books", "Stationery", "Bags"];

interface CategoryFilterProps {
  value: Category | "All";
  onChange: (value: Category | "All") => void;
}

  return (
    <div className="cats">
      {CATEGORIES.map((c) => (
        <button key={c} className={c === value ? "active" : ""} onClick={() => onChange(c)}>
          {c}
        </button>
      ))}
    </div>
  );
}
`,
    "types.ts": `export type Category = "Books" | "Stationery" | "Bags";

  id: number;
  name: string;
  category: Category;
  price: number;
  rating: number;
  inStock: boolean;
}
`,
    "data.ts": `import type { Product } from "./types";

  { id: 1, name: "The Alchemist", category: "Books", price: 299, rating: 4.6, inStock: true },
  { id: 2, name: "Wings of Fire", category: "Books", price: 350, rating: 4.8, inStock: true },
  { id: 3, name: "Atomic Habits", category: "Books", price: 499, rating: 4.7, inStock: false },
  { id: 4, name: "Gel Pen Set", category: "Stationery", price: 120, rating: 4.2, inStock: true },
  { id: 5, name: "A5 Notebook", category: "Stationery", price: 180, rating: 4.4, inStock: true },
  { id: 6, name: "Highlighter Pack", category: "Stationery", price: 150, rating: 4.0, inStock: true },
  { id: 7, name: "Canvas Tote Bag", category: "Bags", price: 399, rating: 4.3, inStock: true },
  { id: 8, name: "Laptop Backpack", category: "Bags", price: 1299, rating: 4.5, inStock: false },
];
`,
    "styles.css": `.shop { max-width: 640px; font-family: sans-serif; }
.shop h1 { margin: 0 0 8px; color: #2b59c3; }
.toolbar { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 12px; }
.search { flex: 1; min-width: 160px; padding: 7px 10px; border: 1px solid #bbb; border-radius: 8px; }
.cats { display: flex; gap: 6px; }
.cats button { padding: 5px 10px; border: 1px solid #bbb; border-radius: 999px; background: white; cursor: pointer; }
.cats button.active { background: #2b59c3; border-color: #2b59c3; color: white; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 10px; }
.card { border: 1px solid #ddd; border-radius: 10px; padding: 10px; display: grid; gap: 4px; background: white; }
.card .name { font-weight: bold; }
.card .meta { color: #666; font-size: 13px; }
.card .price { font-size: 18px; }
.card .out { color: #b42318; font-size: 13px; }
.card button { padding: 5px; border-radius: 6px; border: 1px solid #2b59c3; background: #2b59c3; color: white; cursor: pointer; }
.card button:disabled { background: #ccc; border-color: #ccc; cursor: default; }
.cart { background: #fff3cd; padding: 6px 10px; border-radius: 8px; }
.empty { color: #666; }
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

## Download Project Files

### Download CSR Project Source Code

The complete Product Catalog is ready as a real Vite + React + TypeScript project, with the same code as the final lab in a proper folder structure. It is a pure CSR app: no server or API is needed.

Download CSR Project: [Download Product Catalog Project](/downloads/react/csr-product-catalog.zip)

```
product-catalog/
  package.json        scripts and dependencies
  vite.config.ts      Vite settings
  index.html          the page with the empty root div
  src/
    main.tsx          createRoot + render
    App.tsx           state, filtering and layout
    index.css         styles
    types/            Product and Category types
    data/             the sample products
    components/       ProductCard, ProductList, SearchBar, CategoryFilter, CartSummary
    hooks/            useProductFilter (search + category)
```

### Project Setup Instructions

1. Install **Node.js** 18 or newer from nodejs.org, if you do not have it.
2. Download the ZIP with the link above, and unzip it. You get a folder called `product-catalog`.
3. Open a terminal in that folder (in VS Code: File, Open Folder, then Terminal, New Terminal).
4. Install the packages, then start the app:

```bash
npm install
npm run dev
```

The app opens at `http://localhost:5173`. Press `Ctrl + C` in the terminal to stop it. To make a production build, run `npm run build`.

After the app starts, open the browser's developer tools (F12) and view the page source. You will see only the empty `root` div. That is client-side rendering: all the products are drawn by JavaScript.

## Common mistakes

<div className="tscroll">

| Mistake | What you see | Fix |
|---|---|---|
| Changing a normal variable and expecting the screen to update | Nothing changes on the screen | Use state: `useState` and its setter |
| Forgetting `root.render(<App />)` | An empty page, no errors | Call `render` after `createRoot` |
| Calling `createRoot` twice on the same element | A console warning | Create the root once, then call `root.render` |
| Using the array index as `key` in a list that changes | Inputs or state stay in the wrong row | Use a stable id: `key={item.id}` |
| Storing the filtered list in state as well | The list and the filters get out of sync | Work it out from state on each render (`useMemo` if slow) |
| Fetching or changing the DOM during render | Repeated requests, strange bugs | Put side effects in `useEffect` |

</div>

## Check Yourself

## Exercises

## What to Remember

- Rendering turns components into HTML. In CSR, the browser does it: the server sends an empty `root` div and JavaScript, and `createRoot(...).render(<App />)` starts the app.
- The first render builds everything. Later re-renders follow trigger, render, commit, effects. React diffs the Virtual DOM and changes only what differs; keys match list items.
- State changes, new props and user actions trigger re-renders. CSR is smooth after loading, but the first view is blank until the JavaScript runs.

Next: Server-Side Rendering
