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
reactpackage) 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-dompackage) 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 />)drawsAppinto 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:
- The browser loads
index.html. Therootdiv is empty. - The browser loads and runs
main.tsx(the entry point). createRootconnects React to therootdiv.render(<App />)asks React to drawAppand all its children.- 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:
- Read the HTML and build a first, almost empty page.
- Download the JavaScript bundle.
- Run the JavaScript. React creates the DOM nodes.
- Build the DOM tree from those nodes.
- Layout: work out the size and place of each element.
- 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:
- Trigger. Something asks for an update: the first load, or a state change.
- Render. React calls the components and builds the new Virtual DOM. The screen does not change yet.
- Commit. React applies the changes to the real DOM. Now the user sees them.
- Effects. React runs your
useEffectcode.
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:
- React builds a new Virtual DOM tree.
- It compares it with the old tree.
- 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
divbecamesection. - Type in the first box of each list, then click Add at top. With
key={index}the text stays in the wrong row. Withkey={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
| 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 |
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:
useStateforquery,categoryandcart(a list of product ids).useMemoso the filtered list is worked out again only whenqueryorcategorychange.useCallbacksoaddToCartstays the same function, andmemo(ProductCard)can skip cards that did not change.useEffectfor 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
- Install Node.js 18 or newer from nodejs.org, if you do not have it.
- Download the ZIP with the link above, and unzip it. You get a folder called
product-catalog. - Open a terminal in that folder (in VS Code: File, Open Folder, then Terminal, New Terminal).
- 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
| 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 |
Check Yourself
Exercises
What to Remember
- Rendering turns components into HTML. In CSR, the browser does it: the server sends an empty
rootdiv and JavaScript, andcreateRoot(...).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