Table of Contents
- What is React?
- Virtual DOM and React Fiber
- Modern Build Tools
- JSX and TSX
- Keys in JSX Elements
- Function Components
- React Hooks
- Suspense and Lazy Loading
- React Router
- React Testing
- Best Practices
- React 18+ Features
What is React?
React is a JavaScript library for building user interfaces, particularly web applications. It was created by Facebook (now Meta) and is widely used in production by companies worldwide.
Key Benefits:
- Component-based: Build encapsulated components that manage their own state
- Declarative: Describe what the UI should look like for any given state
- Efficient: Virtual DOM ensures optimal performance
- Flexible: Can be integrated into existing projects gradually
Virtual DOM and React Fiber
Understanding the Real DOM First
Before we understand Virtual DOM, let’s understand what the Real DOM (Document Object Model) is:
The Real DOM is the actual structure of your webpage that the browser creates. When you write HTML like this:
<div>
<h1>Hello World</h1>
<p>This is a paragraph</p>
</div>
The browser creates a tree structure in memory that looks like this:
div
├── h1 (text: "Hello World")
└── p (text: "This is a paragraph")
The Problem with Real DOM
The Real DOM is slow when you need to update it frequently. Here’s why:
// Every time you do this, the browser has to:
// 1. Find the element
// 2. Update its content
// 3. Recalculate styles
// 4. Repaint the screen
document.getElementById('counter').innerHTML = newCount;
Imagine you have a shopping cart with 100 items, and when you add one item, the browser redraws the entire list – that’s expensive!
What is Virtual DOM?
The Virtual DOM is like a „blueprint“ or „draft“ of the Real DOM, but it lives in JavaScript memory (not in the browser’s DOM).
Think of it like this:
- Real DOM: The actual house you live in (lives in browser memory)
- Virtual DOM: The architectural blueprint/plan of the house (lives in JavaScript memory)
Here’s how Virtual DOM looks in JavaScript:
// This is what Virtual DOM looks like (simplified)
const virtualDOM = {
type: 'div',
props: {
className: 'container'
},
children: [
{
type: 'h1',
props: {},
children: 'Hello World'
},
{
type: 'p',
props: {},
children: 'This is a paragraph'
}
]
};
Virtual DOM vs Real DOM Comparison
Virtual DOM | Real DOM |
---|---|
Lives in JavaScript memory | Lives in browser memory |
Fast to create and update | Slow to update |
Lightweight JavaScript object | Heavy browser element |
Can’t be seen by user | What user actually sees |
Used for calculations | Used for display |
How Virtual DOM Works – Step by Step
Let’s say you have a counter app:
Step 1: Initial Render
const Counter = () => {
const [count, setCount] = useState(0);
return (
<div>
<h1>Count: {count}</h1>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
};
React creates Virtual DOM:
// Virtual DOM Tree 1 (when count = 0)
{
type: 'div',
children: [
{ type: 'h1', children: 'Count: 0' },
{ type: 'button', children: '+' }
]
}
Step 2: State Changes
When you click the button, count becomes 1:
// Virtual DOM Tree 2 (when count = 1)
{
type: 'div',
children: [
{ type: 'h1', children: 'Count: 1' }, // This changed!
{ type: 'button', children: '+' } // This stayed same
]
}
Step 3: Diffing (Comparing)
React compares Tree 1 vs Tree 2:
- div: No change ✅
- h1: Text changed from „Count: 0“ to „Count: 1“ ⚠️
- button: No change ✅
Step 4: Update Real DOM
React only updates the Real DOM for what actually changed:
// Only this happens in Real DOM:
document.querySelector('h1').textContent = 'Count: 1';
// The div and button are left alone!
React Fiber – The Engine Behind Virtual DOM
React Fiber is like the „smart worker“ that manages Virtual DOM updates. Think of it as a project manager who decides:
- Which updates are urgent (like user typing)
- Which can wait (like loading a big list)
- How to break big tasks into smaller chunks
Key Fiber Features:
- Incremental Rendering: Instead of updating everything at once, Fiber can pause and resume work
-
Priority-based Updates:
- 🔥 Immediate: User clicks, typing (can’t wait)
- ⚡ High: Animations (should be smooth)
- 📝 Normal: Data updates (can wait a bit)
- 🐌 Low: Hidden content (wait until free time)
- Time Slicing: Fiber breaks work into 5ms chunks
Practical Example:
const ProductCatalog = () => {
const [searchTerm, setSearchTerm] = useState('');
const [products, setProducts] = useState([]);
// Without Fiber: Typing would freeze when filtering 10,000 products
// With Fiber: Search box stays responsive while filtering happens in background
return (
<div>
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search products..."
/>
<ProductGrid
products={products.filter(p =>
p.name.toLowerCase().includes(searchTerm.toLowerCase())
)}
/>
</div>
);
};
Key Takeaways:
- Virtual DOM = Fast Draft: JavaScript copy of your webpage that’s fast to update
- Real DOM = Actual Website: What users see, but slow to update
- Diffing = Smart Comparison: React only changes what actually changed
- Fiber = Smart Manager: Keeps your app responsive by prioritizing important updates
- You Don’t Need to Manage This: React handles all of this automatically!
Modern Build Tools
Vite (Recommended for 2024+)
Vite is the fastest and most modern build tool for React applications.
# Create a new React project with Vite
npm create vite@latest my-react-app -- --template react-ts
cd my-react-app
npm install
npm run dev
Why Vite?
- Lightning-fast hot module replacement (HMR)
- Optimized production builds
- TypeScript support out of the box
- Modern ES modules support
Alternative: Create React App (Legacy)
# Still works but slower than Vite
npx create-react-app my-app --template typescript
JSX and TSX
JSX Basics
JSX allows you to write HTML-like syntax in JavaScript:
// Basic JSX
const greeting = <h1>Hello, World!</h1>;
// JSX with expressions
const name = "Alice";
const element = <h1>Hello, {name}!</h1>;
// JSX with attributes
const image = <img src="photo.jpg" alt="A photo" className="profile-pic" />;
TSX (TypeScript + JSX)
TSX is JSX with TypeScript support, providing better type safety:
// Define component props with TypeScript
interface ButtonProps {
text: string;
onClick: () => void;
disabled?: boolean;
}
const Button: React.FC<ButtonProps> = ({ text, onClick, disabled = false }) => {
return (
<button onClick={onClick} disabled={disabled} className="btn">
{text}
</button>
);
};
Key JSX Rules
-
Single Parent Element: JSX must return a single parent element (use Fragment
<>
if needed) -
className not class: Use
className
instead ofclass
-
Closing Tags: All tags must be closed (
<img />
,<br />
) -
camelCase: Event handlers and attributes use camelCase (
onClick
,onChange
)
// Good JSX
const MyComponent = () => {
return (
<>
<div className="container">
<input onChange={handleChange} />
<img src="image.jpg" alt="description" />
</div>
</>
);
};
Keys in JSX Elements
What are Keys?
Keys are special string attributes you need to include when creating lists of elements. Think of keys like unique ID badges that help React identify which items have changed, been added, or been removed.
// ❌ Without keys
const TodoList = ({ todos }) => (
<ul>
{todos.map(todo => (
<li>{todo.text}</li> // Missing key!
))}
</ul>
);
// ✅ With keys
const TodoList = ({ todos }) => (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li> // Unique key
))}
</ul>
);
Why Keys are Important
1. Performance Optimization
Without keys, React has to guess which items changed:
// Without keys: React recreates ALL items when list changes
const ShoppingCart = ({ items }) => (
<div>
{items.map(item => (
<div>{item.name} - ${item.price}</div> // ❌ No key
))}
</div>
);
// With keys: React only updates changed items
const ShoppingCart = ({ items }) => (
<div>
{items.map(item => (
<div key={item.id}>{item.name} - ${item.price}</div> // ✅ Has key
))}
</div>
);
2. Maintaining Component State
Keys help preserve component state when list order changes:
const StudentList = ({ students }) => (
<div>
{students.map(student => (
<StudentCard
key={student.id} // ✅ Preserves each card's internal state
name={student.name}
isExpanded={false} // This state won't get mixed up
/>
))}
</div>
);
Problems Without Keys
1. Incorrect Updates
// Initial list: ["Apple", "Banana", "Cherry"]
// User removes "Banana"
// New list: ["Apple", "Cherry"]
// ❌ Without keys: React might update wrong items
// React thinks: "First item still Apple ✓, Second item changed Banana→Cherry ❌"
// ✅ With keys: React knows exactly which item was removed
2. Lost Component State
const CommentList = ({ comments }) => (
<div>
{comments.map(comment => (
<Comment
// ❌ Without key: expanding one comment might affect another
text={comment.text}
author={comment.author}
/>
))}
</div>
);
3. Performance Issues
// ❌ Worst case: React recreates 1000 DOM elements
const BigList = ({ items }) => (
<div>
{items.map(item => <ExpensiveItem data={item} />)} // No keys
</div>
);
// ✅ Better: React only updates changed elements
const BigList = ({ items }) => (
<div>
{items.map(item => <ExpensiveItem key={item.id} data={item} />)}
</div>
);
Key Best Practices
1. Use Stable, Unique IDs
// ✅ Good: Stable database ID
<li key={user.id}>{user.name}</li>
// ❌ Bad: Array index (can cause bugs when reordering)
{users.map((user, index) =>
<li key={index}>{user.name}</li>
)}
// ❌ Bad: Random values (causes unnecessary re-renders)
<li key={Math.random()}>{user.name}</li>
2. Combine Values for Unique Keys
// When you don't have unique IDs
const UserPosts = ({ userId, posts }) => (
<div>
{posts.map(post => (
<Post key={`${userId}-${post.id}`} data={post} />
))}
</div>
);
3. Keys Only Need to be Unique Among Siblings
// ✅ Same key can exist in different lists
const App = () => (
<div>
<TodoList todos={todos} /> {/* Can have key="1" */}
<UserList users={users} /> {/* Can also have key="1" */}
</div>
);
Real-world Example:
const ShoppingCart = ({ cartItems, onQuantityChange }) => {
return (
<div className="cart">
{cartItems.map(item => (
<div key={item.productId} className="cart-item">
<img src={item.image} alt={item.name} />
<span>{item.name}</span>
<input
type="number"
value={item.quantity}
onChange={(e) => onQuantityChange(item.productId, e.target.value)}
/>
<span>${item.price * item.quantity}</span>
</div>
))}
</div>
);
};
Remember: Keys are React’s way of tracking list items. Always use stable, unique keys for better performance and fewer bugs!
Function Components
Modern React uses function components exclusively. Class components are legacy.
Basic Function Component
// Simple function component
const Welcome = () => {
return <h1>Welcome to React!</h1>;
};
// Component with props
interface UserProps {
name: string;
age: number;
}
const User: React.FC<UserProps> = ({ name, age }) => {
return (
<div>
<h2>{name}</h2>
<p>Age: {age}</p>
</div>
);
};
Component Composition
const Header = () => <header><h1>My App</h1></header>;
const Footer = () => <footer><p>© 2024</p></footer>;
const App = () => {
return (
<div>
<Header />
<main>
<User name="John" age={30} />
</main>
<Footer />
</div>
);
};
React Hooks
useState Hook – Managing Component Memory
Think of useState as giving your component a „memory“. Without it, every time your component runs, it forgets everything!
Basic useState Pattern:
import { useState } from 'react';
// useState returns 2 things:
// 1. Current value
// 2. Function to update that value
const [currentValue, setCurrentValue] = useState(initialValue);
Simple Counter Example:
const Counter = () => {
const [count, setCount] = useState(0);
return (
<div>
<p>Current count: {count}</p>
<button onClick={() => setCount(count + 1)}>Add 1</button>
<button onClick={() => setCount(count - 1)}>Subtract 1</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
};
useEffect Hook – Doing Things Outside of Rendering
Think of useEffect as a way to tell React: „After you finish drawing the component, also do this extra task.“
Basic useEffect Pattern:
import { useState, useEffect } from 'react';
useEffect(() => {
// Code that runs AFTER component renders
console.log('Component just rendered!');
});
Fetching Data Example:
const UserProfile = ({ userId }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchUser = async () => {
setLoading(true);
try {
const response = await fetch(`/api/users/${userId}`);
const userData = await response.json();
setUser(userData);
} catch (error) {
console.error('Error fetching user:', error);
} finally {
setLoading(false);
}
};
fetchUser();
}, [userId]); // Only run when userId changes
if (loading) return <div>Loading user...</div>;
if (!user) return <div>User not found</div>;
return (
<div>
<h1>{user.name}</h1>
<p>Email: {user.email}</p>
</div>
);
};
useContext Hook – Sharing Data Without Passing Props
Create Context:
import { createContext, useContext, useState } from 'react';
const ThemeContext = createContext({
theme: 'light',
toggleTheme: () => {}
});
Provider Component:
const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
Use Context:
const ThemeButton = () => {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<button
onClick={toggleTheme}
style={{
backgroundColor: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#333' : '#fff'
}}
>
Current theme: {theme}
</button>
);
};
Custom Hooks
Create reusable stateful logic:
// Custom hook for API data fetching
const useApi = <T,>(url: string) => {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(url);
if (!response.ok) throw new Error('Failed to fetch');
const result = await response.json();
setData(result);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
};
// Using the custom hook
const ProductList = () => {
const { data: products, loading, error } = useApi<Product[]>('/api/products');
if (loading) return <div>Loading products...</div>;
if (error) return <div>Error: {error}</div>;
return (
<ul>
{products?.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
);
};
Suspense and Lazy Loading
What is Suspense?
Suspense lets you „pause“ component rendering until something is ready (like data or code). Think of it as a loading state manager that works at the component level.
Lazy Loading Components
Split your app into smaller chunks that load only when needed:
import { lazy, Suspense } from 'react';
// Instead of regular import
// import About from './About';
// Use lazy import - code splits automatically
const About = lazy(() => import('./About'));
const Dashboard = lazy(() => import('./Dashboard'));
const UserProfile = lazy(() => import('./UserProfile'));
Using Suspense with Lazy Components
const App = () => {
const [currentPage, setCurrentPage] = useState('home');
const renderPage = () => {
switch (currentPage) {
case 'about':
return <About />;
case 'dashboard':
return <Dashboard />;
case 'profile':
return <UserProfile />;
default:
return <Home />;
}
};
return (
<div>
<nav>
<button onClick={() => setCurrentPage('home')}>Home</button>
<button onClick={() => setCurrentPage('about')}>About</button>
<button onClick={() => setCurrentPage('dashboard')}>Dashboard</button>
</nav>
{/* Suspense catches loading states of lazy components */}
<Suspense fallback={<div>🔄 Loading page...</div>}>
{renderPage()}
</Suspense>
</div>
);
};
Practical Benefits
// ❌ Without lazy loading: All components load at once
import Dashboard from './Dashboard'; // 500KB
import Analytics from './Analytics'; // 300KB
import Reports from './Reports'; // 400KB
// Total initial bundle: 1.2MB - slow initial load
// ✅ With lazy loading: Components load when needed
const Dashboard = lazy(() => import('./Dashboard')); // Loads only when visited
const Analytics = lazy(() => import('./Analytics')); // Loads only when visited
const Reports = lazy(() => import('./Reports')); // Loads only when visited
// Initial bundle: Much smaller - faster initial load
Suspense with Router
import { BrowserRouter, Routes, Route } from 'react-router-dom';
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const App = () => (
<BrowserRouter>
<Suspense fallback={<div>Loading page... ⏳</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
</BrowserRouter>
);
Multiple Loading States
const App = () => (
<div>
<Header />
{/* Different loading messages for different sections */}
<Suspense fallback={<div>Loading navigation...</div>}>
<Navigation />
</Suspense>
<main>
<Suspense fallback={<div>Loading main content...</div>}>
<MainContent />
</Suspense>
</main>
<Suspense fallback={<div>Loading sidebar...</div>}>
<Sidebar />
</Suspense>
</div>
);
Key Benefits:
- Faster Initial Load: Only load code that’s immediately needed
- Better UX: Show loading states instead of blank screens
- Automatic Code Splitting: Vite/Webpack handles the heavy lifting
- Progressive Loading: Load features as users navigate
React Router
React Router enables navigation in single-page applications:
npm install react-router-dom
Basic Routing Setup
import { BrowserRouter, Routes, Route, Link, useParams, useNavigate } from 'react-router-dom';
const Home = () => <h1>Home Page</h1>;
const About = () => <h1>About Page</h1>;
const UserProfile = () => {
const { userId } = useParams<{ userId: string }>();
return <h1>User Profile: {userId}</h1>;
};
const Navigation = () => (
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/user/123">User 123</Link>
</nav>
);
const App = () => {
return (
<BrowserRouter>
<Navigation />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/user/:userId" element={<UserProfile />} />
<Route path="*" element={<div>Page Not Found</div>} />
</Routes>
</BrowserRouter>
);
};
Programmatic Navigation
const LoginForm = () => {
const navigate = useNavigate();
const handleSubmit = async (credentials: LoginData) => {
try {
await login(credentials);
navigate('/dashboard'); // Redirect after login
} catch (error) {
console.error('Login failed:', error);
}
};
return <form onSubmit={handleSubmit}>...</form>;
};
React Testing
Testing Setup (Jest + React Testing Library):
npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event
Basic Component Testing
// Button.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom';
import { Button } from './Button';
describe('Button Component', () => {
test('renders button with text', () => {
render(<Button text="Click me" onClick={() => {}} />);
expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument();
});
test('calls onClick when clicked', async () => {
const user = userEvent.setup();
const handleClick = jest.fn();
render(<Button text="Click me" onClick={handleClick} />);
await user.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
test('is disabled when disabled prop is true', () => {
render(<Button text="Click me" onClick={() => {}} disabled={true} />);
expect(screen.getByRole('button')).toBeDisabled();
});
});
Best Practices
1. Component Organization
// Good: Small, focused components
const UserCard = ({ user }: { user: User }) => (
<div className="user-card">
<UserAvatar src={user.avatar} />
<UserInfo name={user.name} email={user.email} />
</div>
);
2. State Management
// Keep state close to where it's used
const TodoApp = () => {
const [todos, setTodos] = useState<Todo[]>([]);
const addTodo = (text: string) => {
setTodos(prev => [...prev, { id: Date.now(), text, completed: false }]);
};
return (
<div>
<TodoForm onSubmit={addTodo} />
<TodoList todos={todos} />
</div>
);
};
3. Performance Optimization
import { memo, useMemo, useCallback } from 'react';
// Memoize expensive calculations
const ExpensiveComponent = ({ items }: { items: Item[] }) => {
const expensiveValue = useMemo(() => {
return items.reduce((sum, item) => sum + item.value, 0);
}, [items]);
return <div>Total: {expensiveValue}</div>;
};
// Memoize components
const TodoItem = memo(({ todo, onToggle }: TodoItemProps) => {
return (
<div onClick={() => onToggle(todo.id)}>
{todo.text}
</div>
);
});
React 18+ Features
Automatic Batching
React 18 automatically groups multiple state updates for better performance:
const AutomaticBatching = () => {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
const handleClick = () => {
// All these updates are batched together
// Component only re-renders ONCE at the end
setCount(c => c + 1);
setFlag(f => !f);
// Before React 18: 2 re-renders
// React 18: 1 re-render ✨
};
return (
<div>
<p>Count: {count}</p>
<p>Flag: {flag.toString()}</p>
<button onClick={handleClick}>Update</button>
</div>
);
};
useTransition Hook
Mark updates as non-urgent to keep your app responsive:
import { useState, useTransition } from 'react';
const ProductSearch = ({ products }) => {
const [searchTerm, setSearchTerm] = useState('');
const [filteredProducts, setFilteredProducts] = useState(products);
const [isPending, startTransition] = useTransition();
const handleSearch = (value) => {
setSearchTerm(value); // ⚡ Urgent update - input stays responsive
startTransition(() => {
// 🐌 Non-urgent update - can be interrupted
const filtered = products.filter(p =>
p.name.toLowerCase().includes(value.toLowerCase())
);
setFilteredProducts(filtered);
});
};
return (
<div>
<input
type="text"
value={searchTerm}
onChange={(e) => handleSearch(e.target.value)}
/>
{isPending && <div>🔄 Filtering...</div>}
<ProductList products={filteredProducts} />
</div>
);
};
Next Steps
After mastering these concepts, consider exploring:
- State Management Libraries: Redux Toolkit, Zustand, or Jotai
- Styling Solutions: Tailwind CSS, styled-components, or CSS Modules
- Meta-Frameworks: Next.js for full-stack React applications
- Component Libraries: Material-UI, Chakra UI, or Ant Design
- Testing: Advanced testing patterns and E2E testing with Playwright
- Performance: React DevTools Profiler and advanced optimization techniques
Conclusion
This tutorial covers the essential modern React concepts needed for product development. Focus on:
- Function components with hooks
- TypeScript for type safety
- Modern build tools like Vite
- Proper use of keys in lists
- Suspense and lazy loading for performance
- React 18+ concurrent features
- Testing best practices
- Performance optimization
Practice building small projects to solidify these concepts, and gradually work your way up to larger applications. The React ecosystem is vast, but mastering these fundamentals will give you a solid foundation for any React project.