JSX, props, and state form the foundation of React development. JSX allows us to write HTML-like syntax in JavaScript, props enable component communication, and state manages dynamic data within components. In this lesson, we'll master these fundamental concepts that make React so powerful and intuitive.
JSX (JavaScript XML) is a syntax extension for JavaScript that allows you to write HTML-like code within your JavaScript files. It's not a separate language - it's syntactic sugar that gets transformed into regular JavaScript function calls.
Before JSX, React developers had to write code like this:
// Without JSX - the old way
return React.createElement(
'div',
{ className: 'container' },
React.createElement('h1', null, 'Hello World'),
React.createElement('p', null, 'Welcome to React')
);
With JSX, the same code becomes much more readable:
// With JSX - the modern way
return (
<div className="container">
<h1>Hello World</h1>
<p>Welcome to React</p>
</div>
);
Every JSX expression must have exactly one parent element:
// ❌ This won't work - multiple root elements
return (
<h1>Title</h1>
<p>Content</p>
);
// ✅ Wrap in a fragment
return (
<>
<h1>Title</h1>
<p>Content</p>
</>
);
// ✅ Or wrap in a div
return (
<div>
<h1>Title</h1>
<p>Content</p>
</div>
);
All tags must be properly closed, including self-closing tags:
// ❌ Missing closing tag
return <img src="logo.png">
// ❌ Self-closing tag needs slash
return <br>
// ✅ Properly closed
return <img src="logo.png" />
return <br />
HTML attributes use kebab-case, but JSX uses camelCase:
// ❌ HTML-style attributes
return <div class="container" onclick="handleClick()">
// ✅ JSX-style attributes
return <div className="container" onClick={handleClick}>
Embed JavaScript expressions using curly braces:
const name = "John";
const age = 25;
const isStudent = true;
return (
<div>
<h1>Hello, {name}!</h1>
<p>Age: {age}</p>
<p>Status: {isStudent ? 'Student' : 'Not a student'}</p>
<p>Next year: {age + 1}</p>
</div>
);
Some HTML attributes are reserved JavaScript keywords and need alternative names:
// HTML vs JSX attribute names
class → className
for → htmlFor
tabindex → tabIndex
readonly → readOnly
Props (short for "properties") are read-only data passed from parent components to child components. They enable component communication and make components reusable.
// Parent component
function App() {
return (
<div>
<Welcome name="Alice" age={30} />
<Welcome name="Bob" age={25} />
</div>
);
}
// Child component receiving props
function Welcome(props) {
return (
<div>
<h1>Hello, {props.name}!</h1>
<p>You are {props.age} years old.</p>
</div>
);
}
// Destructuring in function parameters
function Welcome({ name, age }) {
return (
<div>
<h1>Hello, {name}!</h1>
<p>You are {age} years old.</p>
</div>
);
}
// Destructuring in function body
function Welcome(props) {
const { name, age } = props;
return (
<div>
<h1>Hello, {name}!</h1>
<p>You are {age} years old.</p>
</div>
);
}
function Welcome({ name = "Guest", age = 0 }) {
return (
<div>
<h1>Hello, {name}!</h1>
<p>You are {age} years old.</p>
</div>
);
}
// Usage
<Welcome /> // Shows "Hello, Guest! You are 0 years old."
<Welcome name="Alice" /> // Shows "Hello, Alice! You are 0 years old."
Props are read-only - a component cannot modify its own props:
function Counter({ initialCount }) {
// ❌ Cannot modify props directly
// props.initialCount = 5; // This will cause an error
// ✅ Use state instead (we'll cover this next)
const [count, setCount] = useState(initialCount);
return <div>Count: {count}</div>;
}
While JavaScript doesn't have built-in prop validation, you can use TypeScript or PropTypes:
// With TypeScript
interface WelcomeProps {
name: string;
age?: number; // Optional prop
}
function Welcome({ name, age = 0 }: WelcomeProps) {
return (
<div>
<h1>Hello, {name}!</h1>
<p>You are {age} years old.</p>
</div>
);
}
State is data that a component manages and can change over time. Unlike props, state is mutable and controlled by the component itself. When state changes, React re-renders the component to reflect the new data.
| Characteristic | Props | State |
|---|---|---|
| Source | Passed from parent | Managed within component |
| Mutability | Immutable | Mutable |
| Purpose | Component communication | Internal data management |
| Changes trigger | Parent re-render | setState calls |
The useState hook is the primary way to manage state in functional components:
import { useState } from 'react';
function Counter() {
// useState returns an array with two elements:
// 1. Current state value
// 2. Function to update the state
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1);
};
const decrement = () => {
setCount(count - 1);
};
return (
<div>
<h1>Count: {count}</h1>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
}
function NameInput() {
const [name, setName] = useState('');
return (
<div>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter your name"
/>
<p>Hello, {name || 'Guest'}!</p>
</div>
);
}
function ToggleButton() {
const [isVisible, setIsVisible] = useState(true);
return (
<div>
<button onClick={() => setIsVisible(!isVisible)}>
{isVisible ? 'Hide' : 'Show'} Content
</button>
{isVisible && <p>This content can be toggled!</p>}
</div>
);
}
function TodoList() {
const [todos, setTodos] = useState([]);
const [input, setInput] = useState('');
const addTodo = () => {
if (input.trim()) {
setTodos([...todos, { id: Date.now(), text: input }]);
setInput('');
}
};
const removeTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
return (
<div>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Add a todo"
/>
<button onClick={addTodo}>Add</button>
<ul>
{todos.map(todo => (
<li key={todo.id}>
{todo.text}
<button onClick={() => removeTodo(todo.id)}>Remove</button>
</li>
))}
</ul>
</div>
);
}
function UserProfile() {
const [user, setUser] = useState({
name: '',
email: '',
age: ''
});
const handleChange = (field, value) => {
setUser(prev => ({
...prev,
[field]: value
}));
};
return (
<div>
<input
placeholder="Name"
value={user.name}
onChange={(e) => handleChange('name', e.target.value)}
/>
<input
placeholder="Email"
value={user.email}
onChange={(e) => handleChange('email', e.target.value)}
/>
<input
type="number"
placeholder="Age"
value={user.age}
onChange={(e) => handleChange('age', e.target.value)}
/>
<p>
{user.name && `Name: ${user.name}`}
{user.email && ` | Email: ${user.email}`}
{user.age && ` | Age: ${user.age}`}
</p>
</div>
);
}
When the new state depends on the previous state, use functional updates:
function Counter() {
const [count, setCount] = useState(0);
// ❌ Problematic with multiple rapid updates
const badIncrement = () => {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
// Only increments by 1, not 3!
};
// ✅ Correct way with functional updates
const goodIncrement = () => {
setCount(prev => prev + 1);
setCount(prev => prev + 1);
setCount(prev => prev + 1);
// Correctly increments by 3
};
return (
<div>
<h1>Count: {count}</h1>
<button onClick={goodIncrement}>Increment by 3</button>
</div>
);
}
Never mutate state directly - always create new objects/arrays:
function ArrayExample() {
const [items, setItems] = useState([1, 2, 3]);
// ❌ Don't do this - mutation
const badAdd = () => {
items.push(4); // Mutates the array directly
setItems(items);
};
// ✅ Do this - immutable update
const goodAdd = () => {
setItems([...items, 4]); // Creates new array
};
// ✅ Removing items
const removeItem = (index) => {
setItems(items.filter((_, i) => i !== index));
};
// ✅ Updating items
const updateItem = (index, newValue) => {
setItems(items.map((item, i) =>
i === index ? newValue : item
));
};
return (
<div>
<ul>
{items.map((item, index) => (
<li key={index}>
{item}
<button onClick={() => removeItem(index)}>Remove</button>
</li>
))}
</ul>
<button onClick={goodAdd}>Add Item</button>
</div>
);
}
React events are named using camelCase and passed as functions:
function ButtonExample() {
const handleClick = () => {
alert('Button clicked!');
};
return (
<button onClick={handleClick}>
Click me
</button>
);
}
function ListExample() {
const items = ['Apple', 'Banana', 'Orange'];
const handleItemClick = (item) => {
alert(`You clicked: ${item}`);
};
return (
<ul>
{items.map(item => (
<li key={item} onClick={() => handleItemClick(item)}>
{item}
</li>
))}
</ul>
);
}
function FormExample() {
const [formData, setFormData] = useState({
email: '',
password: ''
});
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleSubmit = (e) => {
e.preventDefault(); // Prevent form submission
console.log('Form submitted:', formData);
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
placeholder="Email"
required
/>
<input
type="password"
name="password"
value={formData.password}
onChange={handleChange}
placeholder="Password"
required
/>
<button type="submit">Submit</button>
</form>
);
}
// Reusable Card component
function Card({ title, content, footer }) {
return (
<div className="card">
<div className="card-header">
<h2>{title}</h2>
</div>
<div className="card-body">
{content}
</div>
{footer && (
<div className="card-footer">
{footer}
</div>
)}
</div>
);
}
// Using the Card component
function App() {
return (
<div>
<Card
title="Welcome"
content={<p>This is the main content of the card.</p>}
footer={<button>Learn More</button>}
/>
<Card
title="Features"
content={
<ul>
<li>Easy to use</li>
<li>Highly customizable</li>
<li>Responsive design</li>
</ul>
}
/>
</div>
);
}
The children prop allows you to pass components as content:
function Panel({ title, children }) {
const [isOpen, setIsOpen] = useState(true);
return (
<div className="panel">
<div className="panel-header">
<h3>{title}</h3>
<button onClick={() => setIsOpen(!isOpen)}>
{isOpen ? '▼' : '▶'}
</button>
</div>
{isOpen && (
<div className="panel-content">
{children}
</div>
)}
</div>
);
}
// Usage
function App() {
return (
<div>
<Panel title="User Settings">
<form>
<label>Username:</label>
<input type="text" />
<label>Email:</label>
<input type="email" />
</form>
</Panel>
<Panel title="Notifications">
<p>You have 3 new messages</p>
<button>View All</button>
</Panel>
</div>
);
}
// ❌ Too many responsibilities
function UserCard({ user }) {
const [isEditing, setIsEditing] = useState(false);
const [editName, setEditName] = useState(user.name);
return (
<div>
{isEditing ? (
<input
value={editName}
onChange={(e) => setEditName(e.target.value)}
/>
) : (
<h3>{user.name}</h3>
)}
<p>{user.email}</p>
<button onClick={() => setIsEditing(!isEditing)}>
{isEditing ? 'Save' : 'Edit'}
</button>
</div>
);
}
// ✅ Split into smaller components
function UserInfo({ user }) {
return (
<div>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
);
}
function UserEdit({ user, onSave }) {
const [editName, setEditName] = useState(user.name);
return (
<div>
<input
value={editName}
onChange={(e) => setEditName(e.target.value)}
/>
<button onClick={() => onSave(editName)}>Save</button>
</div>
);
}
function UserCard({ user }) {
const [isEditing, setIsEditing] = useState(false);
return (
<div>
{isEditing ? (
<UserEdit
user={user}
onSave={(newName) => {
// Update user logic here
setIsEditing(false);
}}
/>
) : (
<UserInfo user={user} />
)}
<button onClick={() => setIsEditing(!isEditing)}>
{isEditing ? 'Cancel' : 'Edit'}
</button>
</div>
);
}
// ❌ Unclear prop names
function Component({ data, flag, handler }) {
// What do these props mean?
}
// ✅ Clear, descriptive prop names
function UserProfile({ userData, showDetails, onUpdate }) {
// Much clearer what each prop does
}
// ✅ Provide defaults and document expected types
function Button({
children,
variant = 'primary',
size = 'medium',
disabled = false,
onClick
}) {
return (
<button
className={`btn btn-${variant} btn-${size}`}
disabled={disabled}
onClick={onClick}
>
{children}
</button>
);
}
Understanding JSX, props, and state is fundamental to becoming proficient in React. These concepts work together to create dynamic, interactive user interfaces that are maintainable and scalable.
In the next lesson, we'll explore Server vs Client Components in Next.js, building on our understanding of React fundamentals to understand how Next.js extends these concepts for better performance and user experience.