React is a declarative, component-based JavaScript library for building user interfaces. Created at Facebook by Jordan Walke in 2011, open-sourced in May 2013.
React is not a framework — it handles only the view layer. You choose your own routing, state management, and build tools. This flexibility is both its greatest strength and source of decision fatigue.
React maintains a lightweight in-memory representation of the real DOM. When state changes, React builds a new virtual tree, diffs it against the previous one, and applies only the minimal set of real DOM mutations.
JSX is a syntax extension that lets you write HTML-like markup inside JavaScript. It compiles to React.createElement() calls (or the new JSX transform in React 17+).
// JSX expression
const element = (
<div className="greeting">
<h1>Hello, {user.name}!</h1>
<p>You have {unread} messages.</p>
</div>
);
// Compiles to:
const element = React.createElement(
'div',
{ className: 'greeting' },
React.createElement('h1', null,
'Hello, ', user.name, '!'),
React.createElement('p', null,
'You have ', unread, ' messages.')
);
// Embed any JS expression with { }
const list = (
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
// Conditional rendering
function Status({ isOnline }) {
return (
<span>
{isOnline ? '🟢 Online' : '⚪ Offline'}
</span>
);
}
| HTML | JSX Equivalent | Reason |
|---|---|---|
class | className | class is a reserved word in JS |
for | htmlFor | for is a reserved word in JS |
style="color:red" | style={{'{'}color: 'red'{'}'} | Style accepts an object, not a string |
onclick | onClick | camelCase event handlers |
Components are the building blocks of every React UI. They accept inputs (props), manage internal state, and return JSX describing what should appear on screen.
function UserCard({ name, role, avatar }) {
return (
<div className="user-card">
<img src={avatar} alt={name} />
<h2>{name}</h2>
<span className="role">{role}</span>
</div>
);
}
// Arrow function variant
const Badge = ({ label, color }) => (
<span style={{ background: color }}>
{label}
</span>
);
class UserCard extends React.Component {
render() {
const { name, role, avatar } = this.props;
return (
<div className="user-card">
<img src={avatar} alt={name} />
<h2>{name}</h2>
<span>{role}</span>
</div>
);
}
}
// Class components still work but
// functional + hooks is the standard.
React favours composition: build complex UIs by nesting simple components. Use the children prop for generic containers and render props or hooks for shared logic. Class inheritance is almost never needed.
Props are read-only inputs passed from parent to child. React enforces one-way (top-down) data flow, making the application easier to reason about and debug.
// Parent passes props down
function App() {
return (
<Dashboard
user={{ name: 'Alice', id: 42 }}
theme="dark"
onLogout={() => auth.signOut()}
/>
);
}
// Child receives and uses props
function Dashboard({ user, theme, onLogout }) {
return (
<div className={`dash ${theme}`}>
<Header name={user.name} />
<button onClick={onLogout}>Log Out</button>
</div>
);
}
import PropTypes from 'prop-types';
Dashboard.propTypes = {
user: PropTypes.shape({
name: PropTypes.string.isRequired,
id: PropTypes.number,
}),
theme: PropTypes.oneOf(['light','dark']),
onLogout: PropTypes.func.isRequired,
};
function Panel({ title, children }) {
return (
<section className="panel">
<h2>{title}</h2>
{children}
</section>
);
}
<Panel title="Settings">
<ToggleSwitch />
</Panel>
useStateState is mutable data that belongs to a component. When state changes, React re-renders the component. The useState hook is the primary way to add local state to a function component.
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
<button onClick={() => setCount(0)}>
Reset
</button>
</div>
);
}
// Functional update (prev state)
setCount(prev => prev + 1);
// Object state — spread to preserve fields
const [form, setForm] = useState({
name: '', email: '', age: 0
});
function updateName(name) {
setForm(prev => ({ ...prev, name }));
}
// Array state — immutable patterns
const [items, setItems] = useState([]);
const addItem = (item) =>
setItems(prev => [...prev, item]);
const removeItem = (id) =>
setItems(prev =>
prev.filter(i => i.id !== id));
React 18 automatically batches all state updates — including those in promises, timeouts, and event handlers — into a single re-render for better performance. In React 17, only synthetic event handlers were batched.
useEffect & Side EffectsuseEffect lets you run side effects in function components — data fetching, subscriptions, DOM mutations, and timers. It replaces componentDidMount, componentDidUpdate, and componentWillUnmount.
// Runs after EVERY render
useEffect(() => { /* ... */ });
// Runs ONCE on mount
useEffect(() => {
fetchData();
}, []);
// Runs when `userId` changes
useEffect(() => {
fetchUser(userId);
}, [userId]);
// Cleanup function (unmount / before re-run)
useEffect(() => {
const ws = new WebSocket(url);
ws.onmessage = handleMessage;
return () => ws.close(); // cleanup
}, [url]);
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false;
setLoading(true);
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
if (!cancelled) {
setUser(data);
setLoading(false);
}
});
return () => { cancelled = true; };
}, [userId]);
if (loading) return <Spinner />;
return <h1>{user.name}</h1>;
}
Common mistake: forgetting dependencies causes stale closures; adding object literals as deps causes infinite loops. Use the eslint-plugin-react-hooks exhaustive-deps rule.
React wraps native DOM events in SyntheticEvent objects for cross-browser consistency. Events are delegated to the root container (React 17+) rather than individual DOM nodes.
function SearchBar() {
const [query, setQuery] = useState('');
const handleSubmit = (e) => {
e.preventDefault(); // prevent form reload
search(query);
};
return (
<form onSubmit={handleSubmit}>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
<button type="submit">Go</button>
</form>
);
}
// Passing data to handlers
function TodoList({ todos, onDelete }) {
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
{todo.text}
<button onClick={() => onDelete(todo.id)}>
Delete
</button>
</li>
))}
</ul>
);
}
// Memoised handler (avoid re-renders)
const handleClick = useCallback(
(id) => dispatch({ type: 'DELETE', id }),
[dispatch]
);
| Event | React Prop | When Fires |
|---|---|---|
| click | onClick | Element is clicked |
| change | onChange | Input value changes (fires on every keystroke in React) |
| submit | onSubmit | Form is submitted |
| keydown | onKeyDown | A key is pressed |
| focus / blur | onFocus / onBlur | Element gains / loses focus |
// Ternary operator
{isLoggedIn ? <Dashboard /> : <Login />}
// Logical AND (short-circuit)
{hasError && <ErrorBanner msg={error} />}
// Early return
function Page({ user }) {
if (!user) return <Redirect to="/login" />;
return <Profile user={user} />;
}
// Switch-like with object map
const STATUS_ICONS = {
success: <CheckIcon />,
error: <AlertIcon />,
loading: <Spinner />,
};
return STATUS_ICONS[status] ?? null;
function ProductList({ products }) {
if (products.length === 0) {
return <p>No products found.</p>;
}
return (
<ul>
{products.map(product => (
<li key={product.id}>
<h3>{product.name}</h3>
<span>${product.price}</span>
</li>
))}
</ul>
);
}
Keys help React identify which items changed. Use a stable, unique id — never use array index as key when items can be reordered, added, or removed. Bad keys cause subtle bugs: wrong inputs retain values, animations break, and components lose state.
function SignupForm() {
const [form, setForm] = useState({
email: '', password: '', agree: false
});
const [errors, setErrors] = useState({});
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setForm(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value,
}));
};
const handleSubmit = (e) => {
e.preventDefault();
const errs = validate(form);
if (Object.keys(errs).length) {
setErrors(errs);
} else {
submitForm(form);
}
};
return (
<form onSubmit={handleSubmit}>
<input name="email" value={form.email}
onChange={handleChange} />
{errors.email && <span>{errors.email}</span>}
<input name="password" type="password"
value={form.password}
onChange={handleChange} />
<label>
<input name="agree" type="checkbox"
checked={form.agree}
onChange={handleChange} />
I agree to terms
</label>
<button type="submit">Sign Up</button>
</form>
);
}
function QuickSearch() {
const inputRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
search(inputRef.current.value);
};
return (
<form onSubmit={handleSubmit}>
<input ref={inputRef}
defaultValue="" />
<button type="submit">Go</button>
</form>
);
}
| Aspect | Controlled | Uncontrolled |
|---|---|---|
| Source of truth | React state | DOM |
| Validation | On every change | On submit |
| Dynamic input | Easy | Harder |
| Performance | More re-renders | Fewer re-renders |
useContextContext provides a way to pass data through the component tree without prop drilling. Ideal for global concerns: theme, auth, locale, feature flags.
import { createContext, useState, useContext }
from 'react';
const ThemeContext = createContext('light');
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggle = () =>
setTheme(t => t === 'light' ? 'dark' : 'light');
return (
<ThemeContext.Provider value={{ theme, toggle }}>
{children}
</ThemeContext.Provider>
);
}
// Wrap your app
<ThemeProvider>
<App />
</ThemeProvider>
function ThemeToggle() {
const { theme, toggle } = useContext(ThemeContext);
return (
<button onClick={toggle}>
Current: {theme}
</button>
);
}
function Page() {
const { theme } = useContext(ThemeContext);
return (
<div className={`page ${theme}`}>
<h1>Welcome</h1>
<ThemeToggle />
</div>
);
}
Every consumer re-renders when the context value changes. Split contexts by update frequency (e.g., separate ThemeContext from UserContext). Use useMemo on the value object to avoid unnecessary reference changes.
useReducer & Complex StateuseReducer is an alternative to useState for complex state logic. Inspired by Redux, it centralises state transitions in a pure reducer function.
const initialState = { todos: [], nextId: 1 };
function reducer(state, action) {
switch (action.type) {
case 'ADD':
return {
...state,
todos: [...state.todos, {
id: state.nextId, text: action.text, done: false
}],
nextId: state.nextId + 1,
};
case 'TOGGLE':
return {
...state,
todos: state.todos.map(t =>
t.id === action.id
? { ...t, done: !t.done } : t
),
};
case 'DELETE':
return {
...state,
todos: state.todos.filter(t => t.id !== action.id),
};
default:
throw new Error(`Unknown action: ${action.type}`);
}
}
function TodoApp() {
const [state, dispatch] = useReducer(
reducer, initialState
);
return (
<div>
<AddTodoForm onAdd={(text) =>
dispatch({ type: 'ADD', text })}
/>
{state.todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={() => dispatch({
type: 'TOGGLE', id: todo.id
})}
onDelete={() => dispatch({
type: 'DELETE', id: todo.id
})}
/>
))}
</div>
);
}
Custom hooks let you extract and reuse stateful logic across components. A custom hook is simply a function whose name starts with use and that calls other hooks.
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
// Usage
const [theme, setTheme] =
useLocalStorage('theme', 'dark');
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
fetch(url)
.then(r => r.json())
.then(d => {
if (!cancelled) { setData(d); setLoading(false); }
})
.catch(e => {
if (!cancelled) { setError(e); setLoading(false); }
});
return () => { cancelled = true; };
}, [url]);
return { data, loading, error };
}
// Usage
const { data, loading } = useFetch('/api/posts');
eslint-plugin-react-hooks plugin to enforce these rules automaticallyReact Router v6 is the standard routing library for React SPAs. It enables client-side navigation without full-page reloads, with nested layouts, dynamic segments, and data loading.
import { BrowserRouter, Routes, Route, Link }
from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/users">Users</Link>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/users" element={<Users />}>
<Route path=":userId"
element={<UserDetail />} />
</Route>
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
);
}
import { useParams, useNavigate }
from 'react-router-dom';
function UserDetail() {
const { userId } = useParams();
const navigate = useNavigate();
const { data: user } = useFetch(
`/api/users/${userId}`
);
return (
<div>
<button onClick={() => navigate(-1)}>
Back
</button>
<h1>{user?.name}</h1>
</div>
);
}
// Route loader (React Router 6.4+)
const userLoader = async ({ params }) => {
const res = await fetch(
`/api/users/${params.userId}`
);
if (!res.ok) throw new Response('', { status: 404 });
return res.json();
};
Nested routes render inside an <Outlet /> component in the parent, enabling persistent layouts (e.g., sidebar + content).
// Skip re-render if props unchanged
const UserCard = React.memo(({ name, avatar }) => {
return (
<div className="card">
<img src={avatar} alt={name} />
<h3>{name}</h3>
</div>
);
});
// Custom comparator
const Chart = React.memo(ChartInner, (prev, next) =>
prev.data.length === next.data.length &&
prev.data.every((v, i) => v === next.data[i])
);
// Memoise expensive computation
const sorted = useMemo(
() => items.slice().sort((a, b) =>
a.price - b.price),
[items]
);
// Memoise callback identity
const handleDelete = useCallback(
(id) => dispatch({ type: 'DELETE', id }),
[dispatch]
);
import { lazy, Suspense } from 'react';
const Dashboard = lazy(
() => import('./Dashboard')
);
function App() {
return (
<Suspense fallback={<Spinner />}>
<Dashboard />
</Suspense>
);
}
react-windowuseTransition for non-urgent updates (React 18)The React Testing Library (RTL) encourages testing components the way users interact with them — by visible text, roles, and labels rather than implementation details.
import { render, screen, fireEvent }
from '@testing-library/react';
import Counter from './Counter';
test('increments counter on click', () => {
render(<Counter />);
const button = screen.getByRole('button',
{ name: /increment/i });
const display = screen.getByText(/count: 0/i);
fireEvent.click(button);
expect(screen.getByText(/count: 1/i))
.toBeInTheDocument();
});
test('resets to zero', () => {
render(<Counter />);
fireEvent.click(screen.getByText(/increment/i));
fireEvent.click(screen.getByText(/reset/i));
expect(screen.getByText(/count: 0/i))
.toBeInTheDocument();
});
import { render, screen, waitFor }
from '@testing-library/react';
import UserProfile from './UserProfile';
// Mock fetch
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({
name: 'Alice', role: 'Engineer' }),
})
);
test('loads and displays user', async () => {
render(<UserProfile userId={42} />);
expect(screen.getByText(/loading/i))
.toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText('Alice'))
.toBeInTheDocument();
});
});
userEvent over fireEvent for realistic interaction simulationThank you! — Built with Reveal.js · Single self-contained HTML file