Client-Side Rendering in React

By Syncfusion®, Inc. · Last updated

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.

Rendering turns components into what you see on the screen

Components your TSX code

Render React works

Screen HTML the user sees

Rendering is the step between your code and the page.

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.

Client-side rendering: the server sends an almost empty page and a JavaScript file, and React builds the page in the browser

Browser asks for page

Server sends files

index.html

app.js

React builds page index.html has only <div id="root"></div> The page is blank until app.js has loaded and run

All the HTML is made by JavaScript in the browser.

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

<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.

React rendering architecture: components make React elements, the reconciler compares them, and ReactDOM updates the real DOM

Components your code

Virtual DOM elements

Reconciler finds changes

ReactDOM real DOM

React (the core) works out what changed. ReactDOM does the changing.

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:

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.

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.

React application startup: index.html loads main.tsx, which creates a root and renders App

index.html empty #root

main.tsx runs first

createRoot connect React

render draw <App />

Page is filled

each step happens once, when the page opens

This is the start of every Vite React app.

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

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.

Browser rendering workflow: read HTML, download JavaScript, run it, build the DOM, paint the screen

Read HTML parse

Get JS download

Run JS React works

Build DOM nodes

Layout sizes

Paint pixels

The screen stays blank until the paint step at the end.

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.

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.

The render cycle: a trigger starts a render, React commits changes to the DOM, then effects run

1. Trigger state or props

2. Render call components

3. Commit update the DOM

4. Effects useEffect

a new setState starts the cycle again

Render is only calculating. Commit is when the screen changes.

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

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.

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:

{ 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.

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.

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.

Diffing: React compares the old and new Virtual DOM trees and updates only the node that changed Old Virtual DOM New Virtual DOM

div

h1

p: 0

div

h1

p: 1

only <p> changed, so only <p> is updated on the page

The h1 and the div stay as they are. That is why React is fast.

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.

With keys, React knows which item is which when the list changes key = index: React gets confused key = id: React matches items

0: Wings of Fire (new)

1: The Alchemist

2: Atomic Habits

id 4: Wings of Fire

id 1: The Alchemist

id 2: Atomic Habits

Left: every row looks changed. Right: React sees one new row.

Rendering Triggers

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

Three things start a re-render: a state change, new props, and a user action that sets state

State change setCount(1)

New props price=

User action click, type

Re-render

A user action usually calls a setter, so it becomes a state change.

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.

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

AdvantagesLimitations
Instant page changes after loadingBlank page until the JavaScript loads and runs
Rich, app-like interactionSlow first view on weak phones and networks
Hosted as simple static filesSome search engines and link previews see an empty page
Little work for the serverLarge bundles make the wait longer

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.

The Product Catalog component tree: App owns the state and passes it down

App query, category, cart

SearchBar input

CategoryFilter buttons

ProductList grid

ProductCard × many

Data goes down as props. Changes come up through onChange and onAdd.

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.

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.

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:

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

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

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:

(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.

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

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:
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

MistakeWhat you seeFix
Changing a normal variable and expecting the screen to updateNothing changes on the screenUse state: useState and its setter
Forgetting root.render(<App />)An empty page, no errorsCall render after createRoot
Calling createRoot twice on the same elementA console warningCreate the root once, then call root.render
Using the array index as key in a list that changesInputs or state stay in the wrong rowUse a stable id: key={item.id}
Storing the filtered list in state as wellThe list and the filters get out of syncWork it out from state on each render (useMemo if slow)
Fetching or changing the DOM during renderRepeated requests, strange bugsPut side effects in useEffect

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