React Tutorial
React is a JavaScript library for building user interfaces, developed by Meta (Facebook). It uses a component-based architecture and a virtual DOM for fast, declarative UI updates.
What is React?
- Open-source library by Meta - NOT a full framework (just the UI layer).
- Uses components - reusable, self-contained pieces of UI.
- Uses a Virtual DOM - diffs changes and only updates what changed.
- Declarative - you describe what the UI looks like, not how to change it.
- Current version: React 18+ (concurrent features, automatic batching, Suspense).
Setup
Terminal
# Vite (fastest, recommended)
npm create vite@latest my-app -- --template react
cd my-app
npm install
npm run dev
# Create React App (legacy, slower)
npx create-react-app my-app
cd my-app
npm start
# Project structure (Vite)
my-app/
+-- src/
- +-- main.jsx ? entry point
- +-- App.jsx ? root component
- +-- components/ ? reusable components
- +-- assets/
+-- public/
+-- index.html
+-- vite.config.js
JSX - JavaScript XML
JSX
// JSX is transpiled to React.createElement() calls
const element = <h1>Hello, World!</h1>;
// Expressions in JSX (curly braces)
const name = "Alice";
const el = <h1>Hello, {name}!</h1>;
const sum = <p>2 + 2 = {2 + 2}</p>;
// JSX differences from HTML
// class ? className, for ? htmlFor
// Self-closing tags MUST close: <br />, <input />, <img />
// style takes an object with camelCase props
const style = <div style={{ backgroundColor: "red", fontSize: "18px" }}>...</div>;
// One root element (or Fragment)
return (
<>
<h1>Title</h1>
<p>Paragraph</p>
</>
);
// Comments in JSX
{/* This is a JSX comment */}
Components
JSX
// Functional component (current standard)
function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
}
// Arrow function component
const Card = ({ title, children }) => (
<div className="card">
<h2>{title}</h2>
{children}
</div>
);
// Use in parent
function App() {
return (
<>
<Greeting name="Alice" />
<Card title="Welcome">
<p>This is card content</p>
</Card>
</>
);
}
// Named exports (best practice)
export function Button({ label, onClick }) {
return <button onClick={onClick}>{label}</button>;
}
export default App;
useState Hook
JSX
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)}>+</button>
<button onClick={() => setCount(count - 1)}>-</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
}
// Object state
function Profile() {
const [user, setUser] = useState({ name: "Alice", age: 25 });
const birthday = () =>
setUser(prev => ({ ...prev, age: prev.age + 1 })); // spread old state!
return <p>{user.name}, {user.age}</p>;
}
// Array state
function TodoList() {
const [todos, setTodos] = useState(["Buy milk", "Code"]);
const addTodo = (text) =>
setTodos(prev => [...prev, text]);
const removeTodo = (index) =>
setTodos(prev => prev.filter((_, i) => i !== index));
}
useEffect Hook
JSX
import { useState, useEffect } from "react";
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
// Run once when component mounts (empty deps [])
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/users")
.then(res => res.json())
.then(data => { setUsers(data); setLoading(false); });
}, []); // ? dependency array = [] means "run once on mount"
// Run when 'userId' changes
const [userId, setUserId] = useState(1);
useEffect(() => {
fetch(`/api/users/${userId}`).then(...);
}, [userId]); // re-run when userId changes
// Cleanup (unmount / before re-run)
useEffect(() => {
const timer = setInterval(() => console.log("tick"), 1000);
return () => clearInterval(timer); // cleanup function
}, []);
if (loading) return <p>Loading...</p>;
return (
<ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>
);
}
useContext - Global State
JSX
import { createContext, useContext, useState } from "react";
// 1. Create context
const ThemeContext = createContext("light");
// 2. Provide context (wrap app or subtree)
function App() {
const [theme, setTheme] = useState("light");
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Header />
<Main />
</ThemeContext.Provider>
);
}
// 3. Consume context (any depth - no prop drilling)
function Header() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<header className={`header-${theme}`}>
<button onClick={() => setTheme(t => t === "light" ? "dark" : "light")}>
Toggle Theme
</button>
</header>
);
}
React Router v6
Terminal / JSX
npm install react-router-dom
// main.jsx
import { BrowserRouter } from "react-router-dom";
root.render(<BrowserRouter><App /></BrowserRouter>);
// App.jsx
import { Routes, Route, Link, NavLink, Navigate, useParams, useNavigate } from "react-router-dom";
function App() {
return (
<>
<nav>
<Link to="/">Home</Link>
<NavLink to="/about" className={({isActive}) => isActive ? "active" : ""}>About</NavLink>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/users/:id" element={<UserProfile />} />
<Route path="*" element={<Navigate to="/" />} />
</Routes>
</>
);
}
// Access route params
function UserProfile() {
const { id } = useParams();
return <h1>User #{id}</h1>;
}
// Programmatic navigation
function LoginForm() {
const navigate = useNavigate();
const submit = () => navigate("/dashboard");
}
Custom Hooks
JSX
// Custom hook: useFetch
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetch(url)
.then(res => { if (!res.ok) throw new Error(res.status); return res.json(); })
.then(data => { setData(data); setLoading(false); })
.catch(err => { setError(err.message); setLoading(false); });
}, [url]);
return { data, loading, error };
}
// Use it in any component
function UserList() {
const { data: users, loading, error } = useFetch("/api/users");
if (loading) return <Spinner />;
if (error) return <ErrorMsg msg={error} />;
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
// Custom hook: useLocalStorage
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
try { return JSON.parse(localStorage.getItem(key)) ?? initialValue; }
catch { return initialValue; }
});
const set = (v) => { setValue(v); localStorage.setItem(key, JSON.stringify(v)); };
return [value, set];
}
Component Architecture
As React apps grow, component boundaries matter more than syntax. Build small, focused components that own one clear job and receive data through props or context.
- Keep page components responsible for composition and data fetching.
- Keep presentational components focused on display and interaction.
- Extract repeated logic into custom hooks instead of copying useEffect blocks.
- Avoid deeply nesting state in one giant component.
State Flow
Students often struggle because React state feels invisible. Think in one direction: user action updates state, state updates UI.
React
function CartPage() {
const [items, setItems] = useState([]);
function handleRemove(id) {
setItems(current => current.filter(item => item.id !== id));
}
return <CartTable items={items} onRemove={handleRemove} />;
}
Testing Components
React tests should verify behavior, not implementation details. Focus on what a user can see, click, and read.
React Testing Library
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
test('increments counter', async () => {
render(<Counter />);
await userEvent.click(screen.getByRole('button', { name: /increment/i }));
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});