React โ Complete Beginner Tutorial
6 modules ยท 18 examples ยท Click โถ Run Component to see the output
๐ก How to use this tutorial
Read each explanation, study the code, then click โถ Run Component to see the output.
Components & JSX
React builds UIs from components. A component is a JavaScript function that returns JSX โ a syntax that looks like HTML but lives inside JavaScript. JSX is not HTML: you must use className instead of class, and every tag must be closed. Components must return a single root element โ wrap multiple elements in a <div> or an empty fragment (<>...</>). Component names must start with a capital letter.
Your First React Component
A React component is a function that returns JSX. JSX looks like HTML but it is actually JavaScript in disguise. Rules: use className (not class), use htmlFor (not for), all tags must be self-closed (<img />, <input />), and you can only return one root element. Wrap multiple elements in <> and </> (called a Fragment).
// A component is just a JavaScript function
// that returns JSX
function WelcomeCard() {
return (
<div className="card">
<h1>Hello, React! โ๏ธ</h1>
<p>This is my first component.</p>
<p>JSX looks like HTML but it is JavaScript.</p>
</div>
);
}
// Using expressions inside JSX with {}
function UserBadge() {
const name = "Alice";
const role = "Developer";
const isAdmin = true;
return (
<div className="badge">
<h2>{name}</h2>
<p>Role: {role.toUpperCase()}</p>
<p>Admin: {isAdmin ? "Yes" : "No"}</p>
<p>Year: {new Date().getFullYear()}</p>
</div>
);
}
// Fragment โ return multiple elements
function Stats() {
return (
<>
<p>Languages: 30</p>
<p>Examples: 500+</p>
<p>Cost: Free</p>
</>
);
}In JSX, curly braces {} are the escape hatch into JavaScript. Anything inside {} is evaluated as a JavaScript expression: {name}, {2 + 2}, {isAdmin ? "Yes" : "No"}, {new Date().getFullYear()} all work. You can put any expression in braces โ just not statements like if or for.
JSX Rules and Differences from HTML
JSX has subtle but important differences from HTML. Always use className instead of class. Use camelCase for event handlers and style properties. All tags must be closed โ even self-closing ones like <img /> and <br />. Style is an object, not a string. Comments use {/* */} syntax inside JSX.
// โ
Correct JSX
function ProfileCard() {
const styles = {
container: {
padding: '20px',
borderRadius: '12px',
backgroundColor: '#0c1220',
},
title: {
fontSize: '24px',
fontWeight: 'bold',
color: '#61DAFB',
},
};
return (
<div style={styles.container}>
{/* This is a JSX comment */}
{/* className โ NOT class */}
<h2 style={styles.title} className="card-title">
Alice Smith
</h2>
{/* Self-closing tags need the slash */}
<img
src="/images/avatar.png"
alt="Alice's avatar"
width={80}
height={80}
/>
{/* htmlFor โ NOT for */}
<label htmlFor="email">Email:</label>
<input
id="email"
type="email"
placeholder="Enter email"
/>
{/* tabIndex โ NOT tabindex */}
<button tabIndex={0}>
Follow
</button>
</div>
);
}Inline styles in React are objects, not strings. HTML: style="color: red; font-size: 16px" โ React: style={{ color: "red", fontSize: "16px" }}. Notice the double curly braces: the outer {} is the JSX expression, the inner {} is the JavaScript object. Property names are camelCase (fontSize, not font-size).
Props
Props (short for properties) are how you pass data from a parent component to a child component. They are read-only โ a component must never modify its own props. Props flow one way: down from parent to child. You pass props like HTML attributes: <Button color="blue" size={3} />. Inside the component, you receive them as the first argument (usually called props, or destructured directly). The special children prop holds whatever you put between the opening and closing tags.
Passing and Receiving Props
Pass props like HTML attributes. Numbers, booleans, arrays and objects need curly braces: size={3}, active={true}. Strings can use quotes: name="Alice". Inside the component, receive props as a parameter and access them with props.name, or destructure them directly for cleaner code.
// Component receives props as its first argument
function LanguageCard({ name, emoji, level, executable }) {
return (
<div className="card">
<span className="emoji">{emoji}</span>
<h3>{name}</h3>
<p>Level: {level}</p>
{executable && <span className="badge">โถ Executable</span>}
</div>
);
}
// Parent passes props like HTML attributes
function App() {
return (
<div>
<LanguageCard
name="Python"
emoji="๐"
level="Beginner"
executable={true}
/>
<LanguageCard
name="Rust"
emoji="๐ฆ"
level="Advanced"
executable={false}
/>
<LanguageCard
name="JavaScript"
emoji="โก"
level="Beginner"
executable
/>
</div>
);
}
// Default props โ used when prop is not provided
function Greeting({ name = "Friend", color = "blue" }) {
return <h1 style={{ color }}>Hello, {name}!</h1>;
}
// <Greeting /> โ Hello, Friend!
// <Greeting name="Alice" color="red" /> โ Hello, Alice!The {executable && <span>โถ Executable</span>} pattern is called "short-circuit rendering". In JavaScript, true && <Component /> renders the component. false && <Component /> renders nothing. This is the most common way to conditionally show/hide elements in React โ it is cleaner than a ternary when there is no "else" case.
The children Prop
The children prop holds everything you place between a component's opening and closing tags. This lets you build "wrapper" components โ like Card, Modal, or Layout โ that style a container but let the parent decide what goes inside. It is how React achieves composition.
// Card wraps any content passed to it
function Card({ children, title, accent = '#61DAFB' }) {
return (
<div
style={{
border: `2px solid ${accent}`,
borderRadius: '16px',
padding: '20px',
marginBottom: '16px',
}}
>
{title && <h3 style={{ color: accent }}>{title}</h3>}
{children}
</div>
);
}
// Button wrapper
function PrimaryButton({ children, onClick }) {
return (
<button
onClick={onClick}
style={{
background: '#61DAFB',
color: '#000',
padding: '10px 24px',
borderRadius: '8px',
border: 'none',
fontWeight: 'bold',
cursor: 'pointer',
}}
>
{children}
</button>
);
}
// Using the wrapper components
function App() {
return (
<>
<Card title="Welcome" accent="#61DAFB">
<p>This content goes inside the card.</p>
<PrimaryButton>Click me!</PrimaryButton>
</Card>
<Card accent="#48BB78">
<h4>No title card</h4>
<p>Any content works inside a wrapper.</p>
</Card>
</>
);
}The children prop is what makes React so composable. Instead of passing every detail as a prop, you just nest elements. <Card><Button /><Text /></Card> is much cleaner than <Card button={<Button/>} text="..."/>. Think of components that use children as "slots" that the parent fills in.
State & useState
State is data that changes over time โ a counter, a typed value, a toggle, a list of items. When state changes, React automatically re-renders the component with the new values. useState is the hook that adds state to a function component. It returns a pair: the current value and a setter function. Always use the setter โ never mutate state directly. State is local to each component instance.
useState โ Basic Counter
useState(initialValue) returns an array with two items: the current state value and a function to update it. By convention, destructure them as [value, setValue]. Calling setValue(newValue) tells React to re-render the component with the new value. The component function runs again with the updated state.
import { useState } from 'react';
function Counter() {
// useState returns [currentValue, setter]
const [count, setCount] = useState(0);
// NEVER do: count = count + 1 โ this won't work
// ALWAYS use the setter function
const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);
const reset = () => setCount(0);
// Functional update โ safer when new state depends on old
const doubleIncrement = () => setCount(prev => prev + 2);
return (
<div>
<h2>Count: {count}</h2>
<button onClick={increment}>+ Add 1</button>
<button onClick={decrement}>โ Subtract 1</button>
<button onClick={doubleIncrement}>++ Add 2</button>
<button onClick={reset}>Reset</button>
{count > 10 && <p>๐ Over 10!</p>}
{count < 0 && <p>โ ๏ธ Negative number!</p>}
</div>
);
}When your new state depends on the previous state, use the functional form: setCount(prev => prev + 1) instead of setCount(count + 1). This is important because React can batch state updates โ if you call setCount(count + 1) multiple times in one event, count might be stale. The functional form always uses the freshest value.
State with Objects and Arrays
State can hold any JavaScript value โ objects, arrays, strings, booleans. When state is an object or array, you must create a new one instead of mutating the existing one. Use the spread operator (...) to copy the existing state and only change what needs to change. React compares state by reference, not value.
import { useState } from 'react';
function ProfileEditor() {
const [user, setUser] = useState({
name: 'Alice',
role: 'Developer',
active: true,
});
// Spread operator โ copy all fields, change only one
const updateName = () => setUser({
...user, // keep role and active
name: 'Alice Smith',
});
const toggleActive = () => setUser({
...user,
active: !user.active,
});
return (
<div>
<p>Name: {user.name}</p>
<p>Role: {user.role}</p>
<p>Status: {user.active ? 'Active' : 'Inactive'}</p>
<button onClick={updateName}>Update Name</button>
<button onClick={toggleActive}>Toggle Status</button>
</div>
);
}
// Arrays in state
function TodoList() {
const [todos, setTodos] = useState(['Buy groceries', 'Study React']);
const addTodo = () => setTodos([...todos, 'New task']);
const removeTodo = (index) => setTodos(
todos.filter((_, i) => i !== index) // keep all except index
);
return (
<ul>
{todos.map((todo, i) => (
<li key={i}>
{todo} <button onClick={() => removeTodo(i)}>โ</button>
</li>
))}
<button onClick={addTodo}>Add task</button>
</ul>
);
}Never do this: user.name = "Alice Smith"; setUser(user). React checks if state changed by reference โ if you mutate the object and pass the same reference, React thinks nothing changed and does NOT re-render. Always create a new object/array with the spread operator or array methods like filter and map.
Events & Forms
React handles browser events with camelCase prop names: onClick, onChange, onSubmit, onMouseOver. You pass a function reference โ not a function call. onClick={handleClick} is correct. onClick={handleClick()} is wrong (it calls the function immediately during render). For forms, React prefers "controlled inputs" where the input value is driven by state, giving you full control over what the user types.
Click Events and Event Objects
Pass a function to onClick โ React calls it when clicked. The function receives a synthetic event object (e) with properties like e.target, e.preventDefault(), and e.stopPropagation(). For buttons that need to pass data, use an arrow function: onClick={() => handleDelete(item.id)}.
import { useState } from 'react';
function EventDemo() {
const [log, setLog] = useState([]);
const addLog = (message) => {
setLog(prev => [...prev, message]);
};
// Simple click handler
const handleClick = () => addLog('Button clicked!');
// Click with event object
const handleMouseEnter = (e) => {
addLog(`Mouse over: ${e.target.textContent}`);
};
// Pass data to handler
const handleItemClick = (name) => {
addLog(`Selected: ${name}`);
};
const languages = ['Python', 'React', 'Rust'];
return (
<div>
<button onClick={handleClick}>
Click me
</button>
<button onMouseEnter={handleMouseEnter}>
Hover over me
</button>
{languages.map((lang) => (
<button
key={lang}
onClick={() => handleItemClick(lang)}
>
{lang}
</button>
))}
<div className="log">
{log.map((entry, i) => (
<p key={i}>{entry}</p>
))}
</div>
</div>
);
}The difference between onClick={handleClick} and onClick={handleClick()} is critical. Without parentheses, you pass the function itself โ React calls it when clicked. With parentheses, you call the function immediately during render, which means the handler runs on every render, not on click. Always pass the function reference, not the call.
Controlled Forms
A controlled input has its value driven by React state. You set value={stateValue} and onChange={(e) => setState(e.target.value)}. This means React is always in control of what the input shows โ perfect for validation, formatting, and programmatic changes. Forms use onSubmit to handle submission.
import { useState } from 'react';
function ContactForm() {
const [form, setForm] = useState({
name: '',
email: '',
message: '',
});
const [submitted, setSubmitted] = useState(false);
// One handler for all fields using the name attribute
const handleChange = (e) => {
setForm({
...form,
[e.target.name]: e.target.value,
});
};
const handleSubmit = (e) => {
e.preventDefault(); // prevent page reload
console.log('Form data:', form);
setSubmitted(true);
};
if (submitted) {
return (
<div>
<p>โ
Thanks, {form.name}! We got your message.</p>
<button onClick={() => setSubmitted(false)}>
Send another
</button>
</div>
);
}
return (
<form onSubmit={handleSubmit}>
<input
name="name"
value={form.name}
onChange={handleChange}
placeholder="Your name"
required
/>
<input
name="email"
type="email"
value={form.email}
onChange={handleChange}
placeholder="Email address"
required
/>
<textarea
name="message"
value={form.message}
onChange={handleChange}
placeholder="Your message"
rows={4}
/>
<p>Characters: {form.message.length} / 500</p>
<button type="submit" disabled={!form.name || !form.email}>
Send Message
</button>
</form>
);
}The [e.target.name]: e.target.value trick uses a computed property key โ the square brackets evaluate the expression as the property name. This means one onChange handler updates any field by matching the input's name attribute to the state key. Far cleaner than a separate handleNameChange, handleEmailChange etc. for every field.
useEffect
useEffect lets you perform side effects in function components โ things like fetching data from an API, setting up timers, subscribing to events, or updating the document title. It runs after the component renders. The dependencies array controls when it re-runs: no array means every render, empty array [] means once on mount, [value] means when value changes. Return a cleanup function to undo side effects when the component unmounts.
useEffect โ Document Title and Timers
useEffect(callback, dependencies) runs the callback after rendering. The dependencies array is the key: [] runs once when the component mounts. [count] runs every time count changes. Returning a function from useEffect is the cleanup โ it runs when the component unmounts or before the effect runs again.
import { useState, useEffect } from 'react';
// Example 1: Update document title when count changes
function DocumentTitleDemo() {
const [count, setCount] = useState(0);
// Runs every time count changes
useEffect(() => {
document.title = `Count: ${count} โ CodeLearn`;
console.log('Title updated:', count);
}, [count]); // โ runs when count changes
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
</div>
);
}
// Example 2: Timer with cleanup
function Timer() {
const [seconds, setSeconds] = useState(0);
const [running, setRunning] = useState(false);
useEffect(() => {
if (!running) return;
// Start interval
const id = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
// Cleanup โ clear interval when effect re-runs or unmounts
return () => clearInterval(id);
}, [running]); // re-run when running changes
return (
<div>
<p>Timer: {seconds}s</p>
<button onClick={() => setRunning(!running)}>
{running ? 'Pause' : 'Start'}
</button>
<button onClick={() => { setRunning(false); setSeconds(0); }}>
Reset
</button>
</div>
);
}Forgetting to return a cleanup function from useEffect is one of the most common React bugs. If you start an interval, subscribe to an event, or open a WebSocket without cleaning up, those processes keep running even after the component is removed โ causing memory leaks and weird behaviour. Always clean up side effects.
Fetching Data with useEffect
Fetching data from an API is the most common use of useEffect. The pattern: run useEffect once on mount (empty [] array), fetch the data inside, store it in state. Handle loading and error states for a good user experience. In real apps, libraries like React Query or SWR simplify this pattern considerably.
import { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// Reset state when userId changes
setLoading(true);
setError(null);
const fetchUser = async () => {
try {
const res = await fetch(
`https://jsonplaceholder.typicode.com/users/${userId}`
);
if (!res.ok) throw new Error('Failed to fetch');
const data = await res.json();
setUser(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchUser();
}, [userId]); // re-fetch when userId changes
if (loading) return <p>Loading user...</p>;
if (error) return <p>Error: {error}</p>;
if (!user) return null;
return (
<div className="profile">
<h2>{user.name}</h2>
<p>๐ง {user.email}</p>
<p>๐ข {user.company.name}</p>
<p>๐ {user.website}</p>
</div>
);
}You cannot make the useEffect callback itself async โ useEffect(async () => {}) causes a warning because async functions return a Promise, but useEffect expects either nothing or a cleanup function. Instead, define an async function inside and call it: const fetchData = async () => {...}; fetchData();
Lists & Conditionals
Rendering lists of data is one of the most common React tasks. Use JavaScript's .map() to transform an array of data into an array of JSX elements. Every element in a list needs a unique key prop โ React uses this to efficiently update only the items that changed. For conditional rendering, use &&, the ternary operator, or an if statement before the return.
Rendering Lists with map() and keys
Transform arrays into JSX with .map(). Every element must have a key prop โ a unique identifier that helps React track which items changed. Use IDs from your data, not array indexes (unless the list never reorders). The key lives on the outermost element returned from map().
import { useState } from 'react';
const LANGUAGES = [
{ id: 1, name: 'HTML', emoji: '๐', level: 'Beginner' },
{ id: 2, name: 'CSS', emoji: '๐จ', level: 'Beginner' },
{ id: 3, name: 'JavaScript', emoji: 'โก', level: 'Beginner' },
{ id: 4, name: 'Python', emoji: '๐', level: 'Beginner' },
{ id: 5, name: 'React', emoji: 'โ๏ธ', level: 'Intermediate' },
{ id: 6, name: 'Rust', emoji: '๐ฆ', level: 'Advanced' },
];
function LanguageGrid() {
const [filter, setFilter] = useState('All');
const levels = ['All', 'Beginner', 'Intermediate', 'Advanced'];
const filtered = filter === 'All'
? LANGUAGES
: LANGUAGES.filter(l => l.level === filter);
return (
<div>
{/* Filter buttons */}
<div>
{levels.map(level => (
<button
key={level}
onClick={() => setFilter(level)}
style={{ fontWeight: filter === level ? 'bold' : 'normal' }}
>
{level}
</button>
))}
</div>
{/* Language list */}
<p>{filtered.length} languages shown</p>
<ul>
{filtered.map(lang => (
<li key={lang.id}>
<span>{lang.emoji}</span>
<strong>{lang.name}</strong>
<em> โ {lang.level}</em>
</li>
))}
</ul>
</div>
);
}Never use the array index as a key if your list can be reordered, filtered, or have items added/removed at positions other than the end. When you reorder items, React matches keys to components โ if keys are indexes, React re-renders everything instead of just moving the DOM nodes, which is slow and causes bugs with input values.
Conditional Rendering
React does not have special template syntax for conditions โ you use JavaScript. Three patterns: the ternary (condition ? yes : no) for if/else, the && operator (condition && element) for if-only, and an if statement before the return for complex logic. null and undefined render nothing.
import { useState } from 'react';
function Dashboard({ user }) {
const [showDetails, setShowDetails] = useState(false);
// if before return โ for complex logic
if (!user) {
return <p>Please log in.</p>;
}
return (
<div>
{/* Ternary โ if/else */}
<h2>
{user.isAdmin ? '๐ Admin' : '๐ค User'}: {user.name}
</h2>
{/* && โ show only when true */}
{user.unreadMessages > 0 && (
<p>๐ฌ {user.unreadMessages} unread messages</p>
)}
{/* Ternary for toggle */}
<button onClick={() => setShowDetails(!showDetails)}>
{showDetails ? 'Hide' : 'Show'} Details
</button>
{showDetails && (
<div className="details">
<p>Email: {user.email}</p>
<p>Joined: {user.joinDate}</p>
{user.isPremium && <p>โญ Premium member</p>}
</div>
)}
{/* Render nothing explicitly */}
{user.isBlocked ? null : (
<button>Send Message</button>
)}
</div>
);
}Beware of rendering 0 with &&. In JavaScript, 0 && anything evaluates to 0 โ and React renders 0 as text. So {items.length && <List />} will show "0" when items is empty. Fix it with a boolean: {items.length > 0 && <List />} or {!!items.length && <List />}. This is one of the most common React gotchas.
You finished the React tutorial!
You can now build React apps with components, props, state, events, effects and lists. These are the exact skills used in production React applications at companies around the world.