Welcome back to the React Mastery Series!
In Part 1, we explored the fundamentals of React, learned about components, understood declarative programming, and created our first React application.
Now it's time to make our components interactive.
A React application isn't just a collection of static components. It needs to respond to user interactions, display dynamic information, and update its interface whenever data changes.
In this article, we'll explore four fundamental concepts that make this possible:
- JSX
- Props
- State
- Events
We'll also cover conditional rendering, lists, keys, and how React handles state updates.
By the end of this article, you'll understand how to build dynamic React components and how data flows through a React application.
Table of Contents
- Understanding JSX
- JSX vs. HTML
- JavaScript Expressions Inside JSX
- JSX Conditional Rendering
- JSX Fragments
- Understanding Props
- Passing Props to Components
- Props Destructuring
- The children Prop
- Props Are Read-Only
- What Is State?
- Why Do We Need State?
- Understanding useState
- How State Updates Work
- Updating Objects in State
- Updating Arrays in State
- Understanding React Re-rendering
- Handling Events
- Passing Arguments to Event Handlers
- Event Objects
- Conditional Rendering in Practice
- Rendering Lists with map()
- Understanding Keys
- Props vs. State
- Practical Project: Interactive User Dashboard
- Common Beginner Mistakes
- What We Learned
- What's Next?
1. Understanding JSX
JSX stands for JavaScript XML.
It is a syntax extension for JavaScript that allows developers to describe UI using HTML-like syntax.
For example:
const element = <h1>Hello React!</h1>;
At first glance, this looks like HTML.
However, JSX is not HTML.
It is a syntax that gets transformed into JavaScript by tools such as Vite's build pipeline.
Let's look at a component:
function App() {
return (
<div>
<h1>Hello React</h1>
<p>Welcome to the application.</p>
</div>
);
}
The component returns JSX describing the interface.
React uses this description to determine what should appear on the screen.
Why JSX?
Without JSX, you can create React elements using JavaScript:
import { createElement } from "react";
const element = createElement(
"h1",
null,
"Hello React"
);
With JSX:
const element = <h1>Hello React</h1>;
JSX makes component structures easier to read and write.
2. JSX vs. HTML
Although JSX looks similar to HTML, there are important differences.
Difference 1: className instead of class
In HTML:
<div class="container">
Hello
</div>
In JSX:
<div className="container">
Hello
</div>
React uses className because class is a reserved JavaScript keyword.
Difference 2: camelCase attributes
Many JSX attributes use camelCase.
HTML:
<button onclick="handleClick()">
Click
</button>
JSX:
<button onClick={handleClick}>
Click
</button>
Notice the uppercase C in onClick.
Difference 3: Self-closing tags
In JSX, elements must be properly closed.
<img src="/logo.png" alt="Logo" />
<input type="text" />
Difference 4: Inline styles
In HTML:
<div style="background-color: red;">
Hello
</div>
In JSX:
<div style={{ backgroundColor: "red" }}>
Hello
</div>
Why are there two sets of curly braces?
- The outer braces indicate a JavaScript expression.
- The inner braces define a JavaScript object.
3. JavaScript Expressions Inside JSX
One of the most useful JSX features is the ability to embed JavaScript expressions using curly braces.
function App() {
const name = "Hamed";
const age = 25;
return (
<div>
<h1>Hello, {name}</h1>
<p>Age: {age}</p>
<p>Next year: {age + 1}</p>
</div>
);
}
Output:
Hello, Hamed
Age: 25
Next year: 26
You can use:
- Variables
- Mathematical expressions
- Function calls
- Ternary expressions
- Array methods
- Object properties
For example:
function App() {
const products = ["Laptop", "Phone", "Tablet"];
return (
<div>
<p>Total products: {products.length}</p>
<p>{products.join(", ")}</p>
</div>
);
}
Expressions vs. Statements
This distinction is important.
JSX curly braces accept JavaScript expressions, not arbitrary statements.
An expression produces a value.
{10 + 20}
A statement performs an action or controls program flow.
For example, you cannot directly write an if statement inside JSX:
// Incorrect
return (
<div>
{if (isLoggedIn) {
"Welcome";
}}
</div>
);
Instead, use a ternary expression or calculate the result before returning JSX.
4. JSX Conditional Rendering
Sometimes the UI should change depending on a condition.
For example:
- Show a login button if the user is logged out.
- Show a dashboard if the user is logged in.
- Display an error message if something goes wrong.
React supports several approaches.
Using the ternary operator
function App() {
const isLoggedIn = true;
return (
<div>
{isLoggedIn ? (
<h1>Welcome back!</h1>
) : (
<h1>Please log in.</h1>
)}
</div>
);
}
Using logical AND
function App() {
const hasNotifications = true;
return (
<div>
{hasNotifications && (
<p>You have new notifications.</p>
)}
</div>
);
}
The expression after && is rendered only when the condition is truthy.
Be careful with numbers:
const count = 0;
return (
<div>
{count && <p>You have notifications</p>}
</div>
);
This can render 0 because JavaScript's && operator returns the first falsy operand.
A safer approach:
{count > 0 && <p>You have notifications</p>}
Using if statements
For more complex conditions, use regular JavaScript before returning JSX.
function Greeting({ isLoggedIn }) {
if (isLoggedIn) {
return <h1>Welcome back!</h1>;
}
return <h1>Please log in.</h1>;
}
5. JSX Fragments
React components must return a single React element or a fragment.
Consider:
function App() {
return (
<h1>Hello</h1>
<p>Welcome</p>
);
}
This is invalid because there are two adjacent JSX elements without a parent.
One solution is to use a div:
function App() {
return (
<div>
<h1>Hello</h1>
<p>Welcome</p>
</div>
);
}
But sometimes we don't want an additional DOM element.
That's where Fragments come in.
function App() {
return (
<>
<h1>Hello</h1>
<p>Welcome</p>
</>
);
}
Fragments group elements without adding an extra DOM node.
You can also explicitly import Fragment:
import { Fragment } from "react";
function App() {
return (
<Fragment>
<h1>Hello</h1>
<p>Welcome</p>
</Fragment>
);
}
6. Understanding Props
Props stands for Properties.
Props allow a parent component to pass data to a child component.
Think of props as the inputs of a component.
Imagine a function:
function greet(name) {
return `Hello, ${name}`;
}
console.log(greet("Hamed"));
The function receives an input and produces an output.
React components work similarly.
function Greeting(props) {
return <h1>Hello, {props.name}</h1>;
}
Now we can pass data to it:
function App() {
return <Greeting name="Hamed" />;
}
The output is:
Hello, Hamed
The parent component provides the data.
The child component receives it through props.
Passing multiple props
function UserCard(props) {
return (
<div>
<h2>{props.name}</h2>
<p>Age: {props.age}</p>
<p>Role: {props.role}</p>
</div>
);
}
function App() {
return (
<UserCard
name="Sarah"
age={28}
role="Frontend Developer"
/>
);
}
Props can contain:
- Strings
- Numbers
- Booleans
- Arrays
- Objects
- Functions
- React elements
For example:
<UserCard
name="Sarah"
age={28}
isActive={true}
skills={["React", "JavaScript", "CSS"]}
/>
7. Props Destructuring
Instead of repeatedly writing props.name, you can use JavaScript destructuring.
Without destructuring:
function UserCard(props) {
return (
<div>
<h2>{props.name}</h2>
<p>{props.age}</p>
</div>
);
}
With destructuring:
function UserCard({ name, age }) {
return (
<div>
<h2>{name}</h2>
<p>{age}</p>
</div>
);
}
This is cleaner and more readable.
You can also provide default values:
function UserCard({
name = "Anonymous",
role = "Developer"
}) {
return (
<div>
<h2>{name}</h2>
<p>{role}</p>
</div>
);
}
If a parent doesn't provide these props, the default values are used.
8. The children Prop
React has a special prop called children.
It represents the content placed between a component's opening and closing tags.
function Card({ children }) {
return (
<div className="card">
{children}
</div>
);
}
Usage:
function App() {
return (
<Card>
<h2>User Profile</h2>
<p>Frontend Developer</p>
<button>View Profile</button>
</Card>
);
}
Everything between <Card> and </Card> becomes the children prop.
This is particularly useful for building reusable layouts, modals, cards, and containers.
9. Props Are Read-Only
This is an important React rule.
A child component should never directly modify its props.
Incorrect:
function UserCard(props) {
props.name = "Updated Name";
return <h1>{props.name}</h1>;
}
Props should be treated as immutable.
Why?
Because the parent owns the data it passes to the child.
The child receives that data but doesn't own the original value.
If the data needs to change, the parent should update it and pass the new value down.
We'll see how this works with state.
10. What Is State?
State is data that a component remembers between renders.
Unlike ordinary local variables, state is managed by React.
When state changes, React schedules a new render so the UI can reflect the updated value.
Imagine a counter.
Initially:
Count: 0
After clicking a button:
Count: 1
The displayed value changes.
How does React know that it needs to update the interface?
We use a Hook called useState.
11. Why Do We Need State?
Let's try implementing a counter using a normal variable.
function Counter() {
let count = 0;
function increment() {
count++;
console.log(count);
}
return (
<div>
<h1>{count}</h1>
<button onClick={increment}>
Increment
</button>
</div>
);
}
Click the button.
The console displays:
1
2
3
But the heading still displays:
0
Why?
Because changing an ordinary JavaScript variable doesn't tell React to render the component again.
Also, whenever the component renders again, its local variables are initialized again.
We need something that:
- Stores data between renders.
- Notifies React when that data changes.
That's exactly what state provides.
12. Understanding useState
useState is a React Hook that allows function components to manage state.
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<h1>Count: {count}</h1>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
Let's break this down.
const [count, setCount] = useState(0);
There are three important parts:
1. count
The current state value.
2. setCount
The function used to request a state update.
3. useState(0)
The initial state value.
The initial value is used when React initializes that state.
When the user clicks the button:
setCount(count + 1);
React schedules a state update.
The component renders again using the updated state.
The array destructuring
Why do we write:
const [count, setCount] = useState(0);
Because useState returns an array containing two values.
Conceptually:
const result = useState(0);
const count = result[0];
const setCount = result[1];
Array destructuring simply makes this easier to read.
13. How State Updates Work
One of the most important things to understand about React is that state is a snapshot for a particular render.
Consider:
function Counter() {
const [count, setCount] = useState(0);
function increment() {
setCount(count + 1);
}
return (
<button onClick={increment}>
{count}
</button>
);
}
When the component renders with count = 0, the event handler created during that render sees that value.
Calling setCount requests an update.
React then renders the component with the new state.
What happens if we update state multiple times?
Consider:
function incrementThreeTimes() {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
}
You might expect the count to increase by three.
But if the current value is 0, all three calls use the same render snapshot.
They effectively request:
setCount(1);
setCount(1);
setCount(1);
The final value is 1, not 3.
The functional updater form
To solve this, use a function that receives the previous state.
function incrementThreeTimes() {
setCount((previousCount) => previousCount + 1);
setCount((previousCount) => previousCount + 1);
setCount((previousCount) => previousCount + 1);
}
Now React processes the updates in sequence.
0 → 1 → 2 → 3
This form is especially useful whenever the next state depends on the previous state.
14. Updating Objects in State
State can contain objects.
const [user, setUser] = useState({
name: "Hamed",
age: 25,
});
Suppose we want to update the user's name.
A common beginner mistake is:
user.name = "Sarah";
setUser(user);
This directly mutates the existing object.
React state should be treated as immutable.
Instead, create a new object.
setUser((previousUser) => ({
...previousUser,
name: "Sarah",
}));
Let's understand the spread operator.
{
...previousUser,
name: "Sarah"
}
It copies the existing properties into a new object and replaces the name property.
The resulting object is:
{
name: "Sarah",
age: 25
}
The original object remains unchanged.
15. Updating Arrays in State
Arrays should also be treated as immutable.
Consider:
const [items, setItems] = useState([
"Laptop",
"Phone"
]);
To add an item:
setItems((previousItems) => [
...previousItems,
"Tablet"
]);
To remove an item:
setItems((previousItems) =>
previousItems.filter((item) => item !== "Phone")
);
To update an item:
setItems((previousItems) =>
previousItems.map((item) =>
item === "Laptop" ? "Gaming Laptop" : item
)
);
Notice how JavaScript array methods work naturally with React state.
Useful methods include:
| Method | Common Use |
|---|---|
map() |
Transform or update items |
filter() |
Remove items |
find() |
Find an item |
slice() |
Create a portion of an array |
concat() |
Create a combined array |
| Spread syntax | Add or copy items |
Avoid methods such as push(), pop(), splice(), and sort() directly on arrays stored in state because they mutate the original array.
If you need sorting, copy the array first:
const sortedItems = [...items].sort();
16. Understanding React Re-rendering
Rendering is the process in which React calls your component to calculate the UI for the current props and state.
A component can render again when:
- Its state changes.
- Its parent renders again.
- Its consumed context value changes.
Consider:
function Counter() {
const [count, setCount] = useState(0);
console.log("Counter rendered");
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
Every time the state changes, React renders the component again.
But remember:
A React render does not automatically mean that the browser DOM is completely recreated.
React compares the resulting UI with the previous one and commits the necessary changes.
This distinction is fundamental to understanding React performance.
17. Handling Events
React supports event handling through JSX attributes.
For example:
function App() {
function handleClick() {
console.log("Button clicked!");
}
return (
<button onClick={handleClick}>
Click me
</button>
);
}
Notice:
onClick={handleClick}
We pass the function itself.
We don't call it immediately.
Incorrect:
<button onClick={handleClick()}>
Click me
</button>
This executes the function during rendering.
React event handlers are functions that run in response to user interactions.
Common events include:
| Event | Purpose |
|---|---|
onClick |
Mouse click |
onChange |
Input value change |
onSubmit |
Form submission |
onMouseEnter |
Mouse enters an element |
onMouseLeave |
Mouse leaves an element |
onKeyDown |
Keyboard key pressed |
onFocus |
Element receives focus |
onBlur |
Element loses focus |
18. Passing Arguments to Event Handlers
Sometimes we need to pass additional data to an event handler.
For example:
function App() {
function greet(name) {
console.log(`Hello, ${name}`);
}
return (
<button onClick={() => greet("Hamed")}>
Greet
</button>
);
}
Why use an arrow function?
Because we want to execute greet("Hamed") only when the user clicks.
The arrow function acts as a wrapper.
We can also pass an item's ID:
function deleteUser(userId) {
console.log("Deleting user:", userId);
}
<button onClick={() => deleteUser(42)}>
Delete
</button>
19. Event Objects
React event handlers receive an event object.
function App() {
function handleClick(event) {
console.log(event);
console.log(event.target);
}
return (
<button onClick={handleClick}>
Click me
</button>
);
}
The event object contains information about the interaction.
For example:
-
event.target: The element where the event originated. -
event.currentTarget: The element whose handler is currently running. -
event.preventDefault(): Prevents the browser's default action. -
event.stopPropagation(): Stops the event from propagating further.
Preventing default behavior
Consider a form:
function App() {
function handleSubmit(event) {
event.preventDefault();
console.log("Form submitted!");
}
return (
<form onSubmit={handleSubmit}>
<button type="submit">
Submit
</button>
</form>
);
}
Without preventDefault(), the browser normally performs its default form submission behavior.
We prevent that behavior when we want to handle submission ourselves.
20. Conditional Rendering in Practice
Let's combine state and conditional rendering.
import { useState } from "react";
function Login() {
const [isLoggedIn, setIsLoggedIn] = useState(false);
return (
<div>
{isLoggedIn ? (
<h1>Welcome back!</h1>
) : (
<h1>Please log in.</h1>
)}
<button
onClick={() => setIsLoggedIn(!isLoggedIn)}
>
Toggle Login
</button>
</div>
);
}
Now the interface changes depending on the state.
The button toggles between logged-in and logged-out UI.
A safer updater when the next value depends on the previous one is:
setIsLoggedIn((previousValue) => !previousValue);
21. Rendering Lists with map()
React works naturally with JavaScript arrays.
Suppose we have:
const products = [
"Laptop",
"Phone",
"Tablet"
];
We can render them using map():
function ProductList() {
const products = [
"Laptop",
"Phone",
"Tablet"
];
return (
<ul>
{products.map((product) => (
<li key={product}>
{product}
</li>
))}
</ul>
);
}
The map() method creates a new array containing React elements.
This is one of the most common patterns in React development.
Rendering objects
Real applications usually work with arrays of objects.
const users = [
{ id: 1, name: "John", role: "Developer" },
{ id: 2, name: "Sarah", role: "Designer" },
{ id: 3, name: "Alex", role: "Manager" }
];
Render them:
function UserList() {
return (
<ul>
{users.map((user) => (
<li key={user.id}>
<h3>{user.name}</h3>
<p>{user.role}</p>
</li>
))}
</ul>
);
}
22. Understanding Keys
When rendering lists, React needs a way to identify individual items.
That's why we use the key prop.
{users.map((user) => (
<UserCard
key={user.id}
name={user.name}
/>
))}
Keys help React understand:
- Which items were added.
- Which items were removed.
- Which items changed.
- Which items stayed the same.
Imagine a list:
Before:
A
B
C
After:
A
X
B
C
Stable keys help React recognize that X was inserted rather than treating every item as a completely different item.
Why not use array indexes?
You might see:
{users.map((user, index) => (
<UserCard key={index} name={user.name} />
))}
This can cause problems when items are reordered, inserted, or deleted.
For example, React may associate existing component state with the wrong item.
Whenever possible, use stable unique identifiers from your data.
Important: key is not a normal prop
<UserCard key={user.id} name={user.name} />
The key helps React identify the element.
It is not automatically available inside UserCard.
If the component needs the ID, pass it separately:
<UserCard
key={user.id}
id={user.id}
name={user.name}
/>
23. Props vs. State
This is one of the most important distinctions in React.
Both props and state influence what a component displays, but they serve different purposes.
| Props | State |
|---|---|
| Passed from a parent | Managed by a component |
| Read-only from the child's perspective | Updated through state setters |
| Used to configure components | Used to remember changing data |
| Parent controls the value | Component manages its state |
Consider:
function UserCard({ name }) {
const [isOnline, setIsOnline] = useState(false);
return (
<div>
<h2>{name}</h2>
<p>
{isOnline ? "Online" : "Offline"}
</p>
<button
onClick={() => setIsOnline((value) => !value)}
>
Toggle Status
</button>
</div>
);
}
Here:
-
nameis a prop. -
isOnlineis state.
The parent decides which user name to pass.
The component manages its own online status.
24. Practical Project: Interactive User Dashboard
Now let's combine everything we've learned.
We'll build a small dashboard containing:
- A user list
- Dynamic user information
- Online/offline status
- A delete button
- Conditional rendering
Step 1: Create the user data
const initialUsers = [
{
id: 1,
name: "Hamed",
role: "Frontend Developer",
isOnline: true
},
{
id: 2,
name: "Sarah",
role: "UI Designer",
isOnline: false
},
{
id: 3,
name: "Alex",
role: "Backend Developer",
isOnline: true
}
];
Step 2: Create the UserCard component
function UserCard({
user,
onDelete,
onToggleStatus
}) {
return (
<div className="user-card">
<h2>{user.name}</h2>
<p>{user.role}</p>
<p>
Status: {user.isOnline ? "Online" : "Offline"}
</p>
<button
onClick={() => onToggleStatus(user.id)}
>
Toggle Status
</button>
<button
onClick={() => onDelete(user.id)}
>
Delete User
</button>
</div>
);
}
Notice that the component doesn't directly modify the user.
Instead, it receives callback functions from its parent.
Step 3: Create the Dashboard component
import { useState } from "react";
function Dashboard() {
const [users, setUsers] = useState(initialUsers);
function deleteUser(userId) {
setUsers((previousUsers) =>
previousUsers.filter(
(user) => user.id !== userId
)
);
}
function toggleUserStatus(userId) {
setUsers((previousUsers) =>
previousUsers.map((user) =>
user.id === userId
? { ...user, isOnline: !user.isOnline }
: user
)
);
}
return (
<div>
<h1>User Dashboard</h1>
<p>Total users: {users.length}</p>
{users.length === 0 ? (
<p>No users available.</p>
) : (
users.map((user) => (
<UserCard
key={user.id}
user={user}
onDelete={deleteUser}
onToggleStatus={toggleUserStatus}
/>
))
)}
</div>
);
}
export default Dashboard;
Step 4: Use Dashboard in App
import Dashboard from "./Dashboard";
function App() {
return <Dashboard />;
}
export default App;
Congratulations!
You've just built an interactive React application using:
- JSX
- Components
- Props
- State
- Events
- Conditional rendering
- Array methods
- Keys
- Component composition
This is the foundation of many real-world React applications.
25. Common Beginner Mistakes
Mistake 1: Calling an event handler during rendering
Incorrect:
<button onClick={handleClick()}>
Click
</button>
Correct:
<button onClick={handleClick}>
Click
</button>
Mistake 2: Mutating state directly
Incorrect:
users.push(newUser);
setUsers(users);
Correct:
setUsers((previousUsers) => [
...previousUsers,
newUser
]);
Mistake 3: Forgetting keys
Incorrect:
users.map((user) => (
<li>{user.name}</li>
))
Correct:
users.map((user) => (
<li key={user.id}>{user.name}</li>
))
Mistake 4: Expecting state to update immediately
Incorrect mental model:
setCount(count + 1);
console.log(count);
The log still sees the current render's state snapshot.
Mistake 5: Modifying props
Props should be treated as read-only.
If data needs to change, update the owning state and pass the new value down.
Mistake 6: Using normal variables for UI state
Ordinary variables don't trigger React renders.
Use useState when a value needs to affect the rendered UI.
Mistake 7: Using index as a key everywhere
Indexes are not always stable identifiers.
Prefer unique IDs from your data.
26. What We Learned
In this article, we explored the concepts that make React applications dynamic.
We learned that:
- JSX describes UI using JavaScript-friendly syntax.
- Props allow parent components to pass data to children.
- State allows components to remember changing information.
-
useStatemanages state in function components. - State updates schedule new renders.
- Event handlers respond to user interactions.
- Conditional rendering displays UI based on conditions.
-
map()renders collections of elements. - Keys help React identify list items.
- Immutable updates help React manage state correctly.
Most importantly, we learned how props, state, and events work together.
User Interaction
↓
Event Handler
↓
State Update
↓
React Render
↓
Updated UI
This is one of the fundamental patterns you'll use throughout your React career.
27. What's Next?
In Part 3: React Hooks — Complete Practical Guide, we'll explore Hooks in much greater depth.
We'll cover:
- What Hooks are and why they exist
- Rules of Hooks
-
useStatein depth -
useEffectand dependency arrays - Effect cleanup
-
useRefand DOM references -
useContextand Context API useReduceruseMemouseCallbackuseIduseTransitionuseDeferredValue- Custom Hooks
- Common Hook mistakes
- Practical projects
We'll also explore how React handles component rendering and why understanding Hook behavior is essential for writing reliable applications.
Continue the React Mastery Series
Part 1: React Fundamentals — From Zero to Components
Part 2: JSX, Props, State & Events
Part 3: React Hooks — Complete Practical Guide
Part 4: React Forms & API Integration
Part 5: React Architecture & Best Practices
Part 6: React Performance Optimization
Part 7: React Internals — Virtual DOM, Fiber & Reconciliation
Part 8: Modern React 19 Features
Part 9: Routing & State Management
Part 10: Testing React Applications
If you're learning React, don't just read the examples.
Run them, change them, break them, and try to understand why they behave the way they do.
That's how you move from knowing React syntax to actually understanding React.
Happy coding!
Top comments (0)