Phase 1: React Fundamentals — Components, Props, State & Hooks

Welcome to Phase 1 of the React & Next.js series! This post teaches you the core building blocks of React — the concepts you'll use in every React application you ever build.
We'll go hands-on immediately. By the end, you'll understand why React exists, how to think in components, and how to manage state and side effects with hooks.
Time commitment: 5–7 days, 1–2 hours daily Prerequisites: Basic JavaScript (variables, functions, arrays, objects, promises)
What You'll Learn
By the end of Phase 1, you'll be able to:
✅ Explain what React is and why it was created
✅ Write JSX and understand how it maps to JavaScript
✅ Build reusable components and compose them together
✅ Pass data between components with props
✅ Manage local UI state with useState
✅ Handle side effects with useEffect
✅ Handle events and form inputs in React
✅ Render lists efficiently with keys
✅ Apply conditional rendering patterns
Setting Up Your Environment
You don't need to configure anything to start learning React. Use one of these options:
Option A: Online playground (zero setup)
Go to react.new — a ready-to-go React sandbox in your browser.
Option B: Local project with Vite (recommended for learning)
npm create vite@latest react-fundamentals -- --template react
cd react-fundamentals
npm install
npm run devOpen http://localhost:5173 and you're running React.
Later in this series (Post RN-4), you'll switch to Next.js. For now, Vite is the fastest way to run plain React locally.
What Is React and Why Does It Exist?
Before writing code, understand the problem React solves.
The Problem: Keeping UI in Sync with Data
Imagine a shopping cart. When a user adds an item, the cart icon count should update, the cart panel should show the new item, and the total price should recalculate — all at once.
With vanilla JavaScript, you'd manually find each DOM element and update it. This becomes a maintenance nightmare as apps grow. Keeping the UI in sync with changing data is the core challenge of frontend development.
React's Solution: Declarative UI
React flips the model. Instead of imperatively updating the DOM, you describe what the UI should look like for a given state, and React figures out the DOM updates.
// Imperative (vanilla JS) — you manage the DOM
document.getElementById('count').textContent = count;
document.getElementById('cart').classList.add('visible');
// Declarative (React) — you describe the result
function Cart({ items }) {
return (
<div className={items.length > 0 ? 'cart visible' : 'cart'}>
<span>{items.length}</span>
</div>
);
}When items changes, React re-renders Cart automatically. You don't touch the DOM.
The Virtual DOM
React maintains a lightweight copy of the DOM in memory — the Virtual DOM. When state changes, React:
- Re-renders the component to a new Virtual DOM tree
- Diffs it against the previous tree (reconciliation)
- Applies only the minimal changes to the real DOM
This is fast. And more importantly, it lets you write declarative code without thinking about DOM manipulation.
JSX: HTML in JavaScript
JSX is the syntax React uses to describe UI. It looks like HTML but compiles to regular JavaScript function calls.
// JSX
const element = <h1 className="title">Hello, React!</h1>;
// What it compiles to (you never write this)
const element = React.createElement('h1', { className: 'title' }, 'Hello, React!');JSX Rules
1. Return a single root element
// ❌ Two sibling elements — invalid
return (
<h1>Title</h1>
<p>Content</p>
);
// ✅ Wrapped in a div
return (
<div>
<h1>Title</h1>
<p>Content</p>
</div>
);
// ✅ Or use a Fragment (no extra DOM node)
return (
<>
<h1>Title</h1>
<p>Content</p>
</>
);2. Use className instead of class
// ❌ HTML attribute
<div class="container">
// ✅ JSX attribute
<div className="container">3. Self-close tags that have no children
<img src="photo.jpg" alt="Photo" />
<input type="text" />
<br />4. JavaScript expressions go inside {}
const name = "Alice";
const age = 30;
return (
<div>
<h1>Hello, {name}!</h1>
<p>Age: {age}</p>
<p>Born: {new Date().getFullYear() - age}</p>
<p>{age >= 18 ? 'Adult' : 'Minor'}</p>
</div>
);5. Attributes use camelCase
<input
type="text"
onChange={handleChange}
autoComplete="off"
tabIndex={1}
/>Components: The Building Blocks
A React component is a JavaScript function that returns JSX. Components are the core unit of React — everything is a component.
Your First Component
function Greeting() {
return <h1>Hello, World!</h1>;
}That's it. A component is just a function. React calls it and puts the returned JSX into the DOM.
Component Rules
- Names must start with a capital letter —
Greeting, notgreeting. React uses this to distinguish components from HTML tags. - Must return JSX (or
nullto render nothing) - Can be used like HTML tags in other components
Composing Components
Components nest inside other components, just like HTML elements:
function Avatar() {
return <img src="/avatar.png" alt="User avatar" className="avatar" />;
}
function UserBio() {
return (
<div className="bio">
<p>Software developer. Coffee enthusiast.</p>
</div>
);
}
function UserCard() {
return (
<div className="card">
<Avatar />
<UserBio />
</div>
);
}
function App() {
return (
<main>
<h1>Team Members</h1>
<UserCard />
<UserCard />
<UserCard />
</main>
);
}This is the React component tree — your entire UI is a tree of components.
Props: Passing Data to Components
Props (short for properties) let you pass data from a parent component to a child. They make components reusable.
Basic Props
function Greeting({ name, role }) {
return (
<div>
<h2>Hello, {name}!</h2>
<p>Role: {role}</p>
</div>
);
}
function App() {
return (
<div>
<Greeting name="Alice" role="Admin" />
<Greeting name="Bob" role="Editor" />
<Greeting name="Carol" role="Viewer" />
</div>
);
}Default Props
function Button({ label, variant = 'primary', disabled = false }) {
return (
<button className={`btn btn-${variant}`} disabled={disabled}>
{label}
</button>
);
}
// Uses defaults
<Button label="Save" />
// Overrides defaults
<Button label="Delete" variant="danger" disabled={true} />The children Prop
Any JSX placed between a component's opening and closing tags becomes the children prop:
function Card({ children, title }) {
return (
<div className="card">
<h3 className="card-title">{title}</h3>
<div className="card-body">{children}</div>
</div>
);
}
// Usage
<Card title="Profile">
<p>Name: Alice</p>
<p>Email: alice@example.com</p>
</Card>Props Are Read-Only
Never modify props inside a component. Props flow one way — down from parent to child. A component can only read its props, never change them.
// ❌ Never do this
function BadComponent({ count }) {
count = count + 1; // Mutating props — wrong!
return <p>{count}</p>;
}
// ✅ Use state for values that change
function GoodComponent({ initialCount }) {
const [count, setCount] = useState(initialCount);
return <p>{count}</p>;
}State: Making Components Reactive
Props are external data passed in. State is internal data that a component owns and can change. When state changes, React re-renders the component.
useState
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0); // Initial value: 0
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 returns an array with two items:
- The current state value (
count) - A setter function (
setCount) — call this to update the value
State Updates Are Asynchronous
React batches state updates for performance. Never rely on state being updated immediately after calling the setter:
// ❌ Stale state — count may not reflect latest value
function Counter() {
const [count, setCount] = useState(0);
function handleTripleClick() {
setCount(count + 1); // All three use the same stale count
setCount(count + 1);
setCount(count + 1);
// Result: count goes from 0 to 1, not 3
}
}
// ✅ Functional update — always uses the latest state
function Counter() {
const [count, setCount] = useState(0);
function handleTripleClick() {
setCount(prev => prev + 1); // Uses latest value each time
setCount(prev => prev + 1);
setCount(prev => prev + 1);
// Result: count goes from 0 to 3 ✅
}
}Object State
function ProfileForm() {
const [user, setUser] = useState({
name: '',
email: '',
age: 0,
});
function updateName(newName) {
// ✅ Spread the existing state, then override the changed field
setUser(prev => ({ ...prev, name: newName }));
}
return (
<form>
<input
value={user.name}
onChange={e => setUser(prev => ({ ...prev, name: e.target.value }))}
placeholder="Name"
/>
<input
value={user.email}
onChange={e => setUser(prev => ({ ...prev, email: e.target.value }))}
placeholder="Email"
/>
</form>
);
}Where to Put State
A key React skill is deciding which component should own each piece of state.
Rule: Put state in the lowest common ancestor of all components that need it.
// ❌ State duplicated in two components — they'll get out of sync
function ComponentA() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
function ComponentB() {
const [count, setCount] = useState(0); // Different count!
return <p>Count: {count}</p>;
}
// ✅ State lifted to the parent — single source of truth
function Parent() {
const [count, setCount] = useState(0);
return (
<>
<ComponentA count={count} onIncrement={() => setCount(c => c + 1)} />
<ComponentB count={count} />
</>
);
}This pattern is called lifting state up — it's fundamental to React architecture.
Event Handling
React handles events with camelCase attributes and function references (not strings):
// ❌ HTML style
<button onclick="handleClick()">Click me</button>
// ✅ React style
<button onClick={handleClick}>Click me</button>Event Handler Patterns
function EventExamples() {
// Inline arrow function
const handleClick = () => console.log('Clicked!');
// With event object
const handleInput = (event) => {
console.log(event.target.value);
};
// Passing arguments
const handleItemClick = (id) => {
console.log('Item clicked:', id);
};
return (
<div>
<button onClick={handleClick}>Click me</button>
<input onChange={handleInput} />
{/* ✅ Arrow function to pass arguments */}
<button onClick={() => handleItemClick(42)}>Item 42</button>
{/* ❌ This calls handleItemClick immediately on render! */}
<button onClick={handleItemClick(42)}>Wrong!</button>
</div>
);
}Controlled Inputs
React forms use controlled inputs — the input value is driven by state:
function SearchBar() {
const [query, setQuery] = useState('');
const handleSubmit = (e) => {
e.preventDefault(); // Prevent page reload
console.log('Searching for:', query);
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={query} // Driven by state
onChange={e => setQuery(e.target.value)} // Updates state
placeholder="Search..."
/>
<button type="submit">Search</button>
<p>Query: {query}</p>
</form>
);
}Conditional Rendering
React doesn't have a template syntax like v-if or *ngIf. You use regular JavaScript:
Ternary Operator
function UserStatus({ isLoggedIn }) {
return (
<div>
{isLoggedIn ? (
<p>Welcome back!</p>
) : (
<p>Please log in.</p>
)}
</div>
);
}Logical AND (&&)
Render something only if a condition is true:
function Notification({ hasMessages, messageCount }) {
return (
<div>
<h1>Inbox</h1>
{hasMessages && (
<p>You have {messageCount} unread messages.</p>
)}
</div>
);
}Gotcha:
0 && <Component />renders0, not nothing! Use!!count && ...or a ternary instead.
Early Return
For complex conditions, returning early keeps components readable:
function UserDashboard({ user, isLoading, error }) {
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
if (!user) return null;
return (
<div>
<h1>Welcome, {user.name}</h1>
{/* ... dashboard content */}
</div>
);
}Rendering Lists
Use .map() to transform an array into JSX elements:
const fruits = ['Apple', 'Banana', 'Cherry'];
function FruitList() {
return (
<ul>
{fruits.map(fruit => (
<li key={fruit}>{fruit}</li>
))}
</ul>
);
}The key Prop
Every item in a list needs a unique key prop. React uses keys to track which items changed, were added, or were removed.
const users = [
{ id: 1, name: 'Alice', role: 'Admin' },
{ id: 2, name: 'Bob', role: 'Editor' },
{ id: 3, name: 'Carol', role: 'Viewer' },
];
function UserList() {
return (
<ul>
{users.map(user => (
// ✅ Use a stable, unique ID as key
<li key={user.id}>
{user.name} — {user.role}
</li>
))}
</ul>
);
}Key rules:
- Keys must be unique among siblings (not globally)
- Use stable IDs from your data (database IDs, slugs, etc.)
- Never use array index as key when the list can reorder or items can be added/removed — it causes subtle bugs
// ❌ Index as key — breaks when list reorders
{items.map((item, index) => (
<Item key={index} data={item} />
))}
// ✅ Stable ID as key
{items.map(item => (
<Item key={item.id} data={item} />
))}useEffect: Managing Side Effects
Components should be pure — given the same props and state, they always return the same JSX. But real apps need side effects: fetching data, setting up subscriptions, updating the document title.
useEffect handles all of this.
Basic Usage
import { useState, useEffect } from 'react';
function DocumentTitle({ page }) {
useEffect(() => {
document.title = `Page: ${page}`;
}, [page]); // Run when `page` changes
return <h1>Current page: {page}</h1>;
}The Dependency Array
The second argument to useEffect controls when it runs:
// ✅ Runs after every render
useEffect(() => {
console.log('Rendered');
});
// ✅ Runs once, on mount only
useEffect(() => {
console.log('Mounted');
}, []);
// ✅ Runs when `userId` or `filter` changes
useEffect(() => {
fetchUser(userId, filter);
}, [userId, filter]);Fetching Data
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
setError(null);
fetch(`https://api.example.com/users/${userId}`)
.then(res => {
if (!res.ok) throw new Error('User not found');
return res.json();
})
.then(data => {
setUser(data);
setLoading(false);
})
.catch(err => {
setError(err.message);
setLoading(false);
});
}, [userId]); // Re-fetch when userId changes
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}Cleanup
Some effects need cleanup — to avoid memory leaks and stale updates:
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
// ✅ Cleanup: clear the interval when component unmounts
return () => clearInterval(interval);
}, []); // Empty array — set up once, clean up on unmount
return <p>Time elapsed: {seconds}s</p>;
}function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
let cancelled = false; // Flag to prevent stale updates
fetch(`/api/search?q=${query}`)
.then(res => res.json())
.then(data => {
if (!cancelled) setResults(data); // Only update if still relevant
});
return () => { cancelled = true; }; // Cleanup on next render
}, [query]);
return <ul>{results.map(r => <li key={r.id}>{r.title}</li>)}</ul>;
}Common useEffect Mistakes
// ❌ Missing dependency — stale closure
useEffect(() => {
fetchData(userId); // userId used but not in deps array
}, []); // Bug: never re-fetches when userId changes
// ✅ Include all dependencies
useEffect(() => {
fetchData(userId);
}, [userId]);
// ❌ Object in dependency array — causes infinite loop
useEffect(() => {
fetchData(options);
}, [options]); // options is a new object on every render!
// ✅ Use primitive values or move object inside effect
useEffect(() => {
const options = { limit: 10, page: currentPage };
fetchData(options);
}, [currentPage]);Putting It All Together: A Task Manager
Let's build a small task manager combining everything from this post:
import { useState, useEffect } from 'react';
function TaskItem({ task, onToggle, onDelete }) {
return (
<li className={task.done ? 'done' : ''}>
<input
type="checkbox"
checked={task.done}
onChange={() => onToggle(task.id)}
/>
<span>{task.text}</span>
<button onClick={() => onDelete(task.id)}>Delete</button>
</li>
);
}
function TaskForm({ onAdd }) {
const [text, setText] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (!text.trim()) return;
onAdd(text.trim());
setText('');
};
return (
<form onSubmit={handleSubmit}>
<input
value={text}
onChange={e => setText(e.target.value)}
placeholder="Add a task..."
/>
<button type="submit">Add</button>
</form>
);
}
function TaskManager() {
const [tasks, setTasks] = useState([]);
const [filter, setFilter] = useState('all'); // 'all' | 'active' | 'done'
// Persist tasks to localStorage
useEffect(() => {
const saved = localStorage.getItem('tasks');
if (saved) setTasks(JSON.parse(saved));
}, []);
useEffect(() => {
localStorage.setItem('tasks', JSON.stringify(tasks));
}, [tasks]);
const addTask = (text) => {
setTasks(prev => [
...prev,
{ id: Date.now(), text, done: false },
]);
};
const toggleTask = (id) => {
setTasks(prev =>
prev.map(t => t.id === id ? { ...t, done: !t.done } : t)
);
};
const deleteTask = (id) => {
setTasks(prev => prev.filter(t => t.id !== id));
};
const filteredTasks = tasks.filter(t => {
if (filter === 'active') return !t.done;
if (filter === 'done') return t.done;
return true;
});
return (
<div className="task-manager">
<h1>Tasks ({tasks.filter(t => !t.done).length} remaining)</h1>
<TaskForm onAdd={addTask} />
<div className="filters">
{['all', 'active', 'done'].map(f => (
<button
key={f}
onClick={() => setFilter(f)}
className={filter === f ? 'active' : ''}
>
{f}
</button>
))}
</div>
{filteredTasks.length === 0 ? (
<p>No tasks to show.</p>
) : (
<ul>
{filteredTasks.map(task => (
<TaskItem
key={task.id}
task={task}
onToggle={toggleTask}
onDelete={deleteTask}
/>
))}
</ul>
)}
</div>
);
}
export default TaskManager;This single example demonstrates:
- Multiple components (
TaskItem,TaskForm,TaskManager) - Props passing and children
useStatefor tasks, filter, and form inputuseEffectfor localStorage persistence- Controlled inputs
- Event handlers
- List rendering with keys
- Conditional rendering
The React Developer Mindset
Before moving to Phase 2, internalize these thinking patterns:
Think in Components
Break any UI into a tree of components. Ask yourself:
- What data does this component need? (props)
- What does it own and change? (state)
- What can I extract into a smaller, reusable component?
Data Flows Down, Events Flow Up
Data flows down via props. Changes flow up via callback functions passed as props. Never the other way around.
Keep Components Pure
A component's output should depend only on its props and state — no hidden dependencies, no random values, no directly mutating external data.
State is a Snapshot
When you call setCount(count + 1), React doesn't change count immediately. It schedules a re-render. The next time the component renders, it gets the new count. Think of state as a snapshot of a moment in time.
Practice Exercises
Work through these to cement Phase 1 concepts:
Exercise 1 — Toggle Build a component with a button that toggles between showing and hiding a paragraph of text.
Exercise 2 — Counter with Step Build a counter where the user can set a "step" amount (1, 5, 10) and increment/decrement by that step.
Exercise 3 — Shopping List Build a shopping list where the user can add items, check them off, and delete them. Show a count of unchecked items.
Exercise 4 — Fetch & Display
Fetch a list of posts from https://jsonplaceholder.typicode.com/posts and display them. Show a loading state while fetching and an error state if the request fails.
Exercise 5 — Color Picker Build a component with three sliders (R, G, B, 0–255). Show a colored box that updates in real time as the sliders change.
Summary
In Phase 1, you've learned the foundational React concepts:
| Concept | What It Does |
|---|---|
| JSX | HTML-like syntax that compiles to React.createElement calls |
| Components | Functions that return JSX — the building blocks of React UI |
| Props | Read-only data passed from parent to child |
State (useState) | Internal mutable data — changes trigger re-renders |
useEffect | Runs side effects (fetch, timers, subscriptions) after render |
| Events | Handle user interactions with camelCase event attributes |
| Lists & Keys | Render arrays with .map(), use stable keys |
| Conditional rendering | Ternary, &&, or early return for conditional UI |
These concepts are the complete foundation. Every advanced React pattern builds on them.
What's Next
In Post RN-3: Phase 2 — React Ecosystem, you'll extend this foundation with:
- Client-side routing with React Router
- Global state management
- Form libraries (React Hook Form)
- Data fetching with SWR or TanStack Query
- Component libraries (Tailwind CSS, shadcn/ui)
Series Index
| Post | Title | Status |
|---|---|---|
| RN-1 | React.js & Next.js Roadmap | ✅ Complete |
| RN-2 | Phase 1: React Fundamentals | ✅ You are here |
| RN-3 | Phase 2: React Ecosystem | Coming Soon |
| RN-4 | Phase 3: Next.js Full-Stack | Coming Soon |
| RN-5 | Deep Dive: React Hooks Mastery | Coming Soon |
| RN-6 | Deep Dive: State Management | Coming Soon |
| RN-7 | Deep Dive: App Router & Server Components | Coming Soon |
| RN-8 | Deep Dive: Data Fetching Patterns | Coming Soon |
| RN-9 | Deep Dive: Performance Optimization | Coming Soon |
| RN-10 | Deep Dive: Authentication in Next.js | Coming Soon |
| RN-11 | Deep Dive: Testing React & Next.js | Coming Soon |
| RN-12 | Deep Dive: Deploying Next.js Apps | Coming Soon |
📬 Subscribe to Newsletter
Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.
We respect your privacy. Unsubscribe at any time.
💬 Comments
Sign in to leave a comment
We'll never post without your permission.