This article is published in English.
From DOM Scripts to UI as a Function of State: React's Core Model
Why React replaces manual DOM updates with declarative components, and how JSX, props, state, re-rendering and one-way data flow fit into one mental model.
A like button that updates a counter, swaps an icon and plays an animation is easy to build with plain DOM calls. Fifty of them in a live feed, each also tracking comments, shares and saved state, is where hand-written DOM code starts to collapse. React exists to remove that class of problem: you describe what the screen should look like for the current data, and React works out which DOM changes are needed.
This guide walks through the ideas that make that possible, in the order they build on each other: JSX, components, props, state, re-rendering, the declarative style, and the way data and events move through a component tree. By the end you should be able to predict when a component re-renders, decide where a piece of state belongs, and spot the beginner mistakes that break updates.
The problem React was built to solve
Before component libraries became the norm, interactive UI meant imperative code: find the elements, then issue every change yourself whenever something happens. For a single like button, the setup looks like this:
// Traditional DOM manipulation
const button = document.getElementById("like-button");
const countEl = document.getElementById("like-count");
const icon = document.getElementById("like-icon");
let liked = false;
let count = 42;
and the click handler has to remember every visual detail of both states:
button.addEventListener("click", function() {
if (!liked) {
liked = true;
count++;
countEl.textContent = count;
button.classList.add("liked");
button.classList.remove("unliked");
icon.src = "/icons/heart-filled.svg";
button.style.color = "#e0245e";
// trigger animation
button.classList.add("animate-pop");
setTimeout(() => button.classList.remove("animate-pop"), 300);
} else {
liked = false;
count--;
countEl.textContent = count;
button.classList.remove("liked");
button.classList.add("unliked");
icon.src = "/icons/heart-empty.svg";
button.style.color = "#6c757d";
}
});
That is manageable for one button. Now picture a feed of 50 posts, each with its own like, comment, share and save controls, all of which can change because of user clicks or because new data arrived from the server. The script turns into a web of element lookups, listeners and variables that must be kept in step by hand. Three failure modes appear as the app grows:
- Synchronisation. The DOM shows one thing and your variables say another. Update the counter element but forget the variable, or the reverse, and you now have two sources of truth to debug.
- Reusability. The handler is tied to particular element IDs and a particular HTML structure, so moving the button to another page means rewriting it.
- Complexity. Every new feature forces you to understand everything else it might touch. Small changes break distant parts of the page.
React, open-sourced by Facebook in 2013, answers all three with one idea: stop issuing DOM commands and instead describe the UI that should exist for a given state. React compares that description with what is on screen and applies the difference. You describe; React updates.
JSX: markup that compiles to JavaScript
The first unusual thing in React code is HTML-looking markup inside a function:
function Greeting() {
return (
<div className="greeting">
<h1>Hello, Priya!</h1>
<p>Welcome back.</p>
</div>
);
}
That markup is JSX, a syntax extension that lets you write element trees in JavaScript files. It is neither HTML nor anything the browser understands. A build step (Babel, the TypeScript compiler, or a bundler that uses them) rewrites it into ordinary function calls before the code ever runs. You write:
// What you write (JSX):
return (
<h1 className="title">Hello!</h1>
);
and the compiler produces something equivalent to:
// What the compiler transforms it into:
return React.createElement("h1", { className: "title" }, "Hello!");
React.createElement just returns a plain object describing the element: its type, props and children. React reads these objects to decide what the real DOM should contain. (Newer JSX transforms call a different helper from react/jsx-runtime instead, but the result is the same kind of description object.) Nobody writes these calls by hand; JSX exists because nested markup is far easier to read.
Where JSX differs from HTML
The syntax is close to HTML but not identical. The differences that trip people up most:
// HTML JSX
// ───────────────────────────── ──────────────────────────────────
// class="title" className="title" ← JS reserved word
// for="email" htmlFor="email" ← JS reserved word
// <input> (self-closing optional) <input /> ← must self-close
// onclick="handler()" onClick={handler} ← camelCase, no quotes
// style="color: red" style={{ color: "red" }} ← JS object
class and for are reserved words in JavaScript, so JSX uses className and htmlFor. Every element must be closed. Event handlers are camelCase and receive a function, not a string. Inline styles are objects. For a deeper look at the trade-offs behind this syntax, see why JSX is not HTML.
Embedding JavaScript expressions
Curly braces open a window into JavaScript. Anything that evaluates to a value can go inside: variables, function calls, ternaries, array methods. In this card, the online status is computed first:
function UserCard({ user }) {
const isOnline = user.lastSeen < Date.now() - 5 * 60 * 1000;
and then several expressions shape the markup: a truncated bio, a conditional class name, a conditional label and a formatted date:
return (
<div className="card">
<img src={user.avatar} alt={user.name} />
<h2>{user.name}</h2>
<p>{user.bio.length > 100 ? user.bio.slice(0, 100) + "..." : user.bio}</p>
<span className={isOnline ? "badge-green" : "badge-grey"}>
{isOnline ? "Online" : "Offline"}
</span>
<p>Joined: {new Date(user.joinedAt).toLocaleDateString()}</p>
</div>
);
}
The rule is simple: braces hold expressions, not statements. You can use a ternary but not an if block, and .map() but not a for loop, directly inside the markup.
Components: functions that return UI
A component is a reusable, self-contained piece of UI written as a JavaScript function that returns JSX. That is the whole definition:
// A component is just a function that returns JSX
function Button() {
return (
<button className="btn">
Click me
</button>
);
}
You use it the way you would use an HTML tag, and you can place as many as you like:
function App() {
return (
<div>
<Button />
<Button />
<Button />
</div>
);
}
This renders three identical buttons from a single definition.
Composing small components into larger ones
Components become powerful when they nest. Small, single-purpose pieces are assembled into bigger ones, like building blocks. An avatar only knows how to show an image:
function Avatar({ src, alt }) {
return <img className="avatar" src={src} alt={alt} />;
}
and a chain of slightly larger components builds on it: a name block, a user info row that combines avatar and name, and a post card that combines user info with the post body:
function UserName({ name, handle }) {
return (
<div>
<strong>{name}</strong>
<span>@{handle}</span>
</div>
);
}function UserInfo({ user }) {
return (
<div className="user-info">
<Avatar src={user.avatar} alt={user.name} />
<UserName name={user.name} handle={user.handle} />
</div>
);
}function PostCard({ post, author }) {
return (
<article className="post-card">
<UserInfo user={author} />
<p>{post.content}</p>
<span>{post.likes} likes</span>
</article>
);
}
Each layer has one job. Avatar renders a picture, UserInfo arranges the avatar next to the name, PostCard structures a whole post. That is the habit React encourages: split the interface into the smallest sensible pieces, give each a clear responsibility, and compose. The guide to building reusable React components goes further into component design.
Why component names are capitalised
React tells native elements and components apart by the first letter. <button> becomes a DOM button; <Button> makes React look up a variable called Button in scope and call it. A component named with a lowercase letter is silently treated as an unknown HTML tag, which is a common source of "my component renders nothing" confusion.
Props: configuring a component from outside
A button that always says "Click me" is not very useful. Props (short for properties) are how a parent passes data into a child, so one definition can serve many situations.
Props travel in one direction, from the component that renders another down to the one being rendered, and they arrive as a single object argument. The parent writes them like attributes:
// Parent passes data as props (looks like HTML attributes)
function App() {
return (
<div>
<Button label="Submit" colour="blue" />
<Button label="Cancel" colour="grey" />
<Button label="Delete" colour="red" />
</div>
);
}
and the child destructures what it needs:
// Child receives them as an object
function Button({ label, colour }) {
return (
<button className={`btn btn-${colour}`}>
{label}
</button>
);
}
One definition, three buttons that look different because each received different values.
Props can carry any value
Strings are only the start. Numbers, booleans, arrays, objects and functions are all valid props. A product card, for example, can take a data object, a callback and a flag:
function ProductCard({ product, onAddToCart, featured }) {
return (
<div className={`card ${featured ? "card-featured" : ""}`}>
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>₹{product.price.toLocaleString()}</p>
<p>{product.rating} ★ ({product.reviewCount} reviews)</p>
<button onClick={onAddToCart}>
Add to Cart
</button>
</div>
);
}
At the call site, those values are passed with braces:
// Usage:
<ProductCard
product={{ name: "Headphones", price: 2499, rating: 4.3, reviewCount: 128 }}
onAddToCart={() => addToCart(product.id)}
featured={true}
/>
Note that the inline onAddToCart callback refers to product.id, but product here is only the object literal passed as a prop, not a variable in scope. In real code you would use a variable defined in the parent, for example onAddToCart={() => addToCart(item.id)}. Passing functions down as props is the standard way for children to report events, which comes up again below.
Props are read-only
A component must never change its own props. For the duration of a render, they are fixed inputs. When something needs to change, the parent changes it and renders the child with the new value. Reassigning a prop inside the child is a mistake:
// WRONG — never modify props
function Button({ count }) {
count = count + 1; // ← this is a mistake
return <button>{count}</button>;
}
The reassignment only changes a local variable; it never reaches the parent, and it disappears on the next render. When a component needs a value it can change, that is what state is for:
// CORRECT — props are read-only, use state for data that changes
function Button({ initialCount }) {
const [count, setCount] = useState(initialCount);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
Here initialCount only seeds the state; after the first render, the component owns its count.
State: a component's own memory
Props come from outside. State is data the component owns and can change over time. When state changes, React renders the component again with the new value, and you never touch the DOM yourself.
State is created with the useState hook, imported from React:
import { useState } from "react";
A counter shows the basic shape:
function Counter() {
const [count, setCount] = useState(0);
// ↑ current value ↑ function to update it ↑ initial value return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
}
useState(0) returns a pair: the current value (count, which starts at 0) and a setter (setCount). Calling the setter with a new value tells React to store it and re-render the component.
Rebuilding the like button with state
The imperative like button from the start becomes much smaller. Start with the import:
import { useState } from "react";
and then describe the button for any combination of liked and count:
function LikeButton({ initialLikes }) {
const [liked, setLiked] = useState(false);
const [count, setCount] = useState(initialLikes); function handleClick() {
if (liked) {
setLiked(false);
setCount(c => c - 1);
} else {
setLiked(true);
setCount(c => c + 1);
}
} return (
<button
onClick={handleClick}
className={liked ? "btn-liked" : "btn-default"}
>
{liked ? "♥" : "♡"} {count}
</button>
);
}
There are no element lookups, no classList calls and no textContent assignments. The handler updates two state values, and the JSX says what the button looks like for those values. React makes the DOM match.
Notice the updater form setCount(c => c - 1). Passing a function instead of a value gives you the latest state, which is safer when several updates are queued in the same event.
Each instance keeps its own state
State belongs to a specific instance of a component, not to the component definition. Render three like buttons and each has its own liked and count; clicking one leaves the others alone:
function Feed() {
return (
<div>
<LikeButton initialLikes={24} /> {/* has its own state */}
<LikeButton initialLikes={7} /> {/* has its own state */}
<LikeButton initialLikes={156} /> {/* has its own state */}
</div>
);
}
What belongs in state
Use state for values that:
- change over time because of interaction or incoming data
- should update the UI when they change
- belong to this particular component instance
Keep out of state anything you can calculate from existing props or state (compute it during render instead), and values that change without needing a re-render, such as a timer ID, which fit better in a ref.
How re-rendering works
Re-rendering is the engine behind the whole model. When state changes, React calls your component function again, gets a fresh description of the UI, compares it with the previous one and updates only the parts of the DOM that differ.
On the first render, the counter produces its markup and React creates the DOM nodes:
Initial render:
count = 0
Component runs → returns <p>Count: 0</p> <button>Increment</button>
React creates DOM nodes
When the user clicks, the setter triggers a new render and React compares the two descriptions:
User clicks Increment:
setCount(1) called
React re-renders the component
count = 1
Component runs again → returns <p>Count: 1</p> <button>Increment</button>
React compares: <p>Count: 0</p> vs <p>Count: 1</p>
React updates only the text node inside <p>
Button is unchanged — React leaves it alone
Only the text inside the paragraph changes in the real DOM. The button is left untouched. React does not rebuild the page; it computes the smallest set of changes and applies just those, which is why frequent re-renders are usually fine.
What causes a component to re-render
A component renders again when its own state changes:
1. The component's own state changes (setCount, setUser, etc.)
↓
Component re-renders
It also renders again in a few other situations:
2. The component's props change (parent passes different values)
↓
Component re-renders3. A context the component uses changes
↓
Component re-renders4. The parent re-renders
↓
All children re-render (unless memoized)
In practice, "props changed" and "parent re-rendered" are the same event seen from two sides: a child only receives new props because its parent rendered again. The practical consequence is that, by default, a parent render cascades to all of its children, whether or not their props changed, unless they are memoized.
That cascade is rarely a problem. A render is just a function call producing objects; the expensive part is touching the real DOM, which React keeps to a minimum. Most real performance problems come from how components and state are structured, not from the raw number of renders.
The virtual DOM and reconciliation
React keeps a lightweight JavaScript representation of the UI, often called the virtual DOM. Each render produces a new tree of element objects, and React diffs it against the previous tree in a process called reconciliation. The result of that diff is the list of real DOM operations to perform.
You never work with this layer directly; it is an implementation detail. It matters because comparing plain objects in memory is cheap, while real DOM operations are comparatively slow. Routing every update through this comparison lets React batch and minimise the expensive work.
Declarative versus imperative UI
React is declarative, and understanding that word is the real mental shift. Compare two ways of rendering a list.
Imperative: listing the steps
The imperative version first empties the container:
// Imperative: manual DOM manipulation
const list = document.getElementById("list");
list.innerHTML = ""; // clear it
then creates, configures and appends every item by hand:
items.forEach(item => {
const li = document.createElement("li");
li.textContent = item.name;
li.className = item.active ? "active" : "";
li.addEventListener("click", () => handleClick(item.id));
list.appendChild(li);
});
The code is a sequence of commands: make this node, set that property, attach this listener, insert it there. You are responsible for the before, the after, and every step in between.
Declarative: describing the result
The declarative version says what the list should look like for any items array:
// Declarative: describe the desired output
function ItemList({ items, onItemClick }) {
return (
<ul>
{items.map(item => (
<li
key={item.id}
className={item.active ? "active" : ""}
onClick={() => onItemClick(item.id)}
>
{item.name}
</li>
))}
</ul>
);
}
There is no "clear the list, then rebuild it". You describe the target, and React works out how to get from the current DOM to that target. The key prop tells React which item is which between renders, so it can move, update or remove the right elements instead of recreating them all.
Why the declarative style suits UIs
- Predictability. With the same props and state, a component renders the same output. You can treat the interface as a pure function of your data:
UI = f(state, props). - Less to keep in your head. You never track what the DOM currently looks like or which edits it needs. You only describe the present.
- Fewer bugs. Most manual-DOM bugs, such as updating one element but not its neighbour or listeners drifting out of step with data, simply cannot happen when the whole view is derived from state.
The component tree and one-way data flow
Every React app is a tree. A root component, usually App, renders children, which render their own children, mirroring the structure of the screen:
App
├── Navbar
│ ├── Logo
│ ├── NavLinks
│ └── UserMenu
│ ├── Avatar
│ └── DropdownMenu
├── Dashboard
│ ├── Sidebar
│ │ ├── SidebarLink (×5)
│ │ └── UserStats
│ └── MainContent
│ ├── StatsRow
│ │ ├── StatCard (×4)
│ └── RecentActivity
│ ├── ActivityItem (×10)
└── Footer
Data flows down
Data moves from parent to child through props. A component cannot reach into a sibling's or a parent's state. That one-way flow is what keeps large apps understandable:
App (has user, notifications, theme)
│
├── Navbar (receives: user, notifications)
│ │
│ └── UserMenu (receives: user)
│ │
│ └── Avatar (receives: user.avatar, user.name)
│
└── Dashboard (receives: user, theme)
│
└── MainContent (receives: theme)
Avatar sees only what UserMenu hands it, and UserMenu sees only what Navbar hands it. Nothing leaks sideways or upward, so when a value is wrong you know to look up the chain of parents.
Events flow up
A component deep in the tree cannot change its parent's state directly, but it can call a function the parent passed down. The parent owns the state:
// Parent owns the state and passes down both the value and the updater
function App() {
const [searchQuery, setSearchQuery] = useState("");
and passes both the value and the setter into its children; the search bar calls the setter whenever the input changes:
return (
<div>
<SearchBar
query={searchQuery}
onChange={setSearchQuery} {/* passes the setter as a prop */}
/>
<Results query={searchQuery} />
</div>
);
}// Child receives the updater and calls it on user input
function SearchBar({ query, onChange }) {
return (
<input
value={query}
onChange={e => onChange(e.target.value)} {/* calls parent's setter */}
placeholder="Search..."
/>
);
}
The query lives only in App. SearchBar stores nothing; it reports each keystroke through the onChange prop. Results gets the updated query as a prop and re-renders. Data goes down as props, events go up as function calls. (The explanatory comments inside the opening tags are for reading only; in real JSX, a braced comment belongs among children, and inside a tag you would write a plain /* */ comment or drop it.)
Mistakes that break React updates
Mutating state in place
It is tempting to modify a state object directly and pass it back:
// WRONG — mutating state directly
const [user, setUser] = useState({ name: "Priya", age: 28 });
The first birthday below changes the object and hands the setter the same reference, so the UI does not update. The second creates a new object with the spread operator, which React sees as a change:
function birthday() {
user.age = 29; // ← directly modifying the object
setUser(user); // ← same object reference — React sees no change
} // UI does NOT update// CORRECT — create a new object
function birthday() {
setUser({ ...user, age: user.age + 1 }); // ← new object, React detects change
}
React decides whether state changed by comparing references with Object.is, not by inspecting contents. Change an object or array in place and pass the same reference back, and React concludes nothing happened and may skip the render.
Arrays follow the same rule. Pushing into the existing array fails:
// WRONG — mutating the array
const [items, setItems] = useState([1, 2, 3]);
items.push(4); // ← modifies in place
setItems(items); // ← same reference — no re-render
while spreading into a new array works:
// CORRECT — create a new array
setItems([...items, 4]); // ← spread creates a new array
Mixing up props and state
A quick test decides which is which. Does the value come from a parent? It is a prop and read-only. Does the component own it and change it over time? It is state. Wrapping a value that never changes in useState is a sign of confusion:
// Wrong: using state for something that should be a prop
function UserCard() {
const [userName] = useState("Priya"); // ← why is this state? It never changes here
return <p>{userName}</p>;
}
If the parent controls the data, accept it as a prop:
// Correct: static data the parent controls comes as a prop
function UserCard({ name }) {
return <p>{name}</p>;
}
Storing values you could calculate
Not everything that changes needs its own state. Keeping a count alongside the list it counts duplicates information:
// Wrong: storing derived data in state
const [items, setItems] = useState([...]);
const [itemCount, setItemCount] = useState(0); // ← why is this state?
Now every change to items requires a matching update to itemCount, and a single forgotten update leaves them inconsistent. Computing it during render keeps the two in sync by construction:
// Every time items changes, you have to remember to also update itemCount
// And if you forget once, they're out of sync// Correct: derive it during render
const [items, setItems] = useState([...]);
const itemCount = items.length; // ← just a variable — always in sync
Components that do everything
A single component rendering a sidebar, a table, a form and several modals is hard to read, test and reuse. A useful heuristic: if you cannot say what a component does in one short sentence, it is doing too much.
// Hard to maintain — does everything
function UserDashboard() {
// manages user state
// fetches orders
// handles filters
// manages pagination
// controls modal open/close
// renders sidebar
// renders order table
// renders filter controls
// renders pagination
// renders modal
return ( /* 200 lines of JSX */ );
}
Splitting it by responsibility gives each part a name and a job:
// Better — clear single responsibilities
function UserDashboard() {
return (
<DashboardLayout>
<OrderFilters />
<OrderTable />
<OrderPagination />
<OrderDetailModal />
</DashboardLayout>
);
}
Designing an app out of components
Draw the boundaries before writing code
Start from the design and mark the pieces. Anything that repeats is a component. Anything with one clear job is a component. Things that change together belong together; things that change independently should be split. For a product listing page, the design:
Looking at the design:
[ Filter Bar ]
[ Product Card ][ Product Card ][ Product Card ]
[ Product Card ][ Product Card ][ Product Card ]
[ Pagination ]
breaks down into this tree:
Components:
ProductPage
├── FilterBar
│ ├── FilterGroup (×3)
│ └── SortDropdown
├── ProductGrid
│ └── ProductCard (×N)
└── Pagination
Put state in the lowest common owner
For every piece of state, find the lowest component in the tree that needs it, and keep the state there:
If only ProductCard needs the "expanded" state → put it in ProductCard
If FilterBar and ProductGrid both need filters → put filters in ProductPage
If Pagination and ProductGrid both need currentPage → put it in ProductPage
When two siblings need the same data, move the state into their closest shared parent and pass it down. This is known as lifting state up. Keeping state as low as possible also limits how much of the tree re-renders when it changes.
Build a static version first, then add state
Begin with hardcoded data: no useState, no handlers, just markup driven by props. When the layout is settled, work out which values really change over time and introduce state for exactly those. Step one is a fully static card:
// Step 1: static — no state, hardcoded data
function ProductCard() {
return (
<div className="card">
<img src="/headphones.jpg" alt="Headphones" />
<h3>Wireless Headphones</h3>
<p>₹2,499</p>
<button>Add to Cart</button>
</div>
);
}
Step two replaces the hardcoded content with a product prop, and step three adds a small piece of state for the "Added" confirmation, which resets itself after two seconds:
// Step 2: accept props — still no state
function ProductCard({ product }) {
return (
<div className="card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>₹{product.price.toLocaleString()}</p>
<button>Add to Cart</button>
</div>
);
}// Step 3: add state for interactivity
function ProductCard({ product, onAddToCart }) {
const [added, setAdded] = useState(false); function handleAdd() {
setAdded(true);
onAddToCart(product.id);
setTimeout(() => setAdded(false), 2000);
} return (
<div className="card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>₹{product.price.toLocaleString()}</p>
<button onClick={handleAdd} disabled={added}>
{added ? "Added ✓" : "Add to Cart"}
</button>
</div>
);
}
Because structure and interactivity are separated, each step is easy to check on its own.
Key takeaways
The whole model, condensed. Why React exists:
Why React:
Plain JS DOM manipulation is hard to scale and maintain
React: describe the UI, let React handle DOM updates
and the essentials of each concept covered above, from JSX to common mistakes:
JSX:
HTML-like syntax in JavaScript — compiled to React.createElement()
{} embeds any JavaScript expression
Use className (not class), onClick (not onclick)Components:
Functions that return JSX
Capital letter names (Button, not button)
Reusable, composable building blocksProps:
Data passed from parent to child (like function arguments)
Read-only — a component never modifies its own props
Can be strings, numbers, objects, arrays, functionsState:
const [value, setValue] = useState(initialValue)
Data owned by a component that changes over time
Calling the setter triggers a re-render
Each component instance has its own stateRe-rendering:
Happens when state changes, props change, or parent re-renders
React diffs the virtual DOM and updates only what changed
Not expensive — surgical DOM updatesDeclarative:
Describe what the UI should look like
React figures out what changed and how to update the DOM
UI = f(state, props) — predictable, testableData flow:
Props flow down (parent → child)
Events flow up (child calls parent's function)
One-way data flow keeps the application predictableCommon mistakes:
Mutating state directly (use spread / new objects)
Storing derived values in state (compute them during render)
Overloading one component (split by responsibility)
- Components are functions, props are their arguments, and state is their memory. The UI is always a projection of the current state.
- Re-rendering is cheap by design; the DOM work React saves you is the expensive part. Structure state well before worrying about render counts.
- Always give setters new objects and arrays; React compares references, not contents.
- Keep state in the lowest component that needs it, derive everything else during render, and let events flow up through callback props.
Context, effects, custom hooks and performance tuning all build on these ideas. With a solid grip on components, props, state and re-rendering, the more advanced parts of React become extensions of a model you already understand rather than new rules to memorize.