What is React?
React is a JavaScript library for building user interfaces, created by Facebook/Meta in 2013 and open-sourced. It's the most popular frontend library in the world.
Analogy: Traditional web development is like repainting an entire wall
every time one square inch changes. React is like having smart tiles —
only the changed tile repaints, automatically.
Core idea: Build UIs as a tree of components — small, reusable pieces of UI. Each component is a function that takes props (inputs) and returns JSX (HTML-like syntax that describes the UI). React figures out the minimum DOM updates needed when state changes.
JSX — JavaScript + XML
JSX is a syntax extension that lets you write HTML-like code inside JavaScript:
// JSX
const element = <h1 className="title">Hello, {user.name}!</h1>;
// What it compiles to (React.createElement calls)
const element = React.createElement("h1", { className: "title" }, `Hello, ${user.name}!`);
Key JSX rules:
// 1. Use className, not class (class is a JS keyword)
<div className="card">...</div>
// 2. Self-close tags without children
<input type="text" />
<br />
// 3. Return one root element (or use React Fragment to avoid extra DOM node)
return (
<>
<h1>Title</h1>
<p>Content</p>
</>
);
// 4. JavaScript expressions in {}
<p>2 + 2 = {2 + 2}</p>
<button onClick={() => alert("clicked")}>Click</button>
// 5. Conditional rendering
{isLoggedIn && <UserMenu />}
{isLoggedIn ? <UserMenu /> : <LoginButton />}
// 6. List rendering — always provide a key
{users.map(user => <li key={user.id}>{user.name}</li>)}
Components & Props
A component is a function that returns JSX:
// A simple component
interface CardProps {
title: string;
description: string;
imageUrl?: string;
}
function Card({ title, description, imageUrl }: CardProps) {
return (
<div className="card">
{imageUrl && <img src={imageUrl} alt={title} />}
<h2>{title}</h2>
<p>{description}</p>
</div>
);
}
// Using it
<Card
title="Introduction to React"
description="Learn React from scratch"
imageUrl="/react-thumbnail.jpg"
/>
Props are read-only — a component never modifies its own props. If you need to change data, lift it to state.
useState — Local Component State
import { useState } from "react";
function Counter() {
// useState returns [currentValue, setter]
const [count, setCount] = useState<number>(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
<button onClick={() => setCount(prev => prev - 1)}>-</button>
{/* Use functional update (prev =>) when new state depends on old state */}
</div>
);
}
// State with objects
function Form() {
const [form, setForm] = useState({ name: "", email: "" });
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setForm(prev => ({ ...prev, [e.target.name]: e.target.value }));
// Spread is essential — setState replaces, not merges, for hooks
};
return (
<form>
<input name="name" value={form.name} onChange={handleChange} />
<input name="email" value={form.email} onChange={handleChange} />
</form>
);
}
React's rendering rule: Every setState call schedules a re-render. React batches them (since React 18, all updates are batched, even in async code).
useEffect — Side Effects
useEffect handles operations outside the render cycle: data fetching, subscriptions, timers, DOM manipulation.
import { useState, useEffect } from "react";
function UserProfile({ userId }: { userId: number }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false; // prevent state update after unmount
setLoading(true);
setError(null);
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
if (!cancelled) {
setUser(data);
setLoading(false);
}
})
.catch(err => {
if (!cancelled) {
setError(err.message);
setLoading(false);
}
});
// Cleanup function — runs on unmount or before next effect
return () => { cancelled = true; };
}, [userId]); // dependency array — re-runs when userId changes
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
if (!user) return null;
return <div>{user.name}</div>;
}
Dependency array rules:
[]— runs once on mount, cleanup on unmount (like componentDidMount)[dep1, dep2]— runs when any dependency changes- No array — runs after every render (usually a bug)
useRef
useRef stores a mutable value that persists across renders without causing re-renders:
import { useRef, useEffect } from "react";
// 1. Accessing DOM elements
function SearchInput() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus(); // auto-focus on mount
}, []);
return <input ref={inputRef} type="search" />;
}
// 2. Storing previous values
function Component({ value }: { value: number }) {
const prevValue = useRef(value);
useEffect(() => {
console.log(`Changed from ${prevValue.current} to ${value}`);
prevValue.current = value;
}, [value]);
}
// 3. Storing mutable values that shouldn't trigger re-renders
function Timer() {
const intervalId = useRef<ReturnType<typeof setInterval> | null>(null);
const start = () => {
intervalId.current = setInterval(() => console.log("tick"), 1000);
};
const stop = () => {
if (intervalId.current) clearInterval(intervalId.current);
};
}
useContext — Prop Drilling Solution
Passing props through many layers is called prop drilling — it's painful. Context provides a way to share values across the tree without explicit prop passing.
// 1. Create context
interface ThemeContextType {
theme: "light" | "dark";
toggle: () => void;
}
const ThemeContext = React.createContext<ThemeContextType | null>(null);
// 2. Provide it
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<"light" | "dark">("light");
const toggle = () => setTheme(t => t === "light" ? "dark" : "light");
return (
<ThemeContext.Provider value={{ theme, toggle }}>
{children}
</ThemeContext.Provider>
);
}
// 3. Custom hook for clean usage
function useTheme() {
const context = useContext(ThemeContext);
if (!context) throw new Error("useTheme must be used within ThemeProvider");
return context;
}
// 4. Consume anywhere in the tree
function Header() {
const { theme, toggle } = useTheme();
return <button onClick={toggle}>Current: {theme}</button>;
}
useReducer — Complex State Logic
When useState gets complex (many related state updates, next state depends on current), use useReducer:
type State = { count: number; step: number };
type Action =
| { type: "increment" }
| { type: "decrement" }
| { type: "setStep"; payload: number }
| { type: "reset" };
function reducer(state: State, action: Action): State {
switch (action.type) {
case "increment": return { ...state, count: state.count + state.step };
case "decrement": return { ...state, count: state.count - state.step };
case "setStep": return { ...state, step: action.payload };
case "reset": return { count: 0, step: 1 };
default: return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0, step: 1 });
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
<input
type="number"
value={state.step}
onChange={e => dispatch({ type: "setStep", payload: Number(e.target.value) })}
/>
</div>
);
}
Custom Hooks
Extract reusable stateful logic into custom hooks (functions that start with use):
// Reusable data fetching hook
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(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 — zero boilerplate at the call site
function UserList() {
const { data: users, loading, error } = useFetch<User[]>("/api/users");
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return <ul>{users?.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
Performance Optimization
React.memo — Skip Re-renders
// Without memo: re-renders every time parent re-renders
function ExpensiveList({ items }: { items: string[] }) {
console.log("Rendering list...");
return <ul>{items.map(i => <li key={i}>{i}</li>)}</ul>;
}
// With memo: only re-renders if items prop actually changed
const ExpensiveList = React.memo(({ items }: { items: string[] }) => {
return <ul>{items.map(i => <li key={i}>{i}</li>)}</ul>;
});
useMemo & useCallback
function Parent() {
const [count, setCount] = useState(0);
const [text, setText] = useState("");
// useMemo: only recomputes if items changes (not when count/text changes)
const sortedItems = useMemo(
() => [...items].sort((a, b) => a.localeCompare(b)),
[items]
);
// useCallback: stable function reference (important for memo'd children)
const handleClick = useCallback((id: number) => {
console.log("clicked", id);
}, []); // no deps: same function reference every render
return <ExpensiveList items={sortedItems} onItemClick={handleClick} />;
}
Don't over-optimize: Only add memo/useMemo/useCallback when you have a measured performance problem. They add complexity and have their own overhead.
Common Interview Questions
Practice
- Beginner: Build a controlled form with name, email, and password fields, a submit handler, and basic validation (all fields required, email format, password min 8 chars).
- Core: Implement a
useLocalStorage<T>(key, initialValue)hook that syncs state to localStorage. - Context: Build a shopping cart with CartContext — add/remove items, update quantity, calculate total. Cart count shows in the navbar.
- Performance: Given a list of 10,000 items with a search filter, optimize it so filtering is fast using
useMemoand the list doesn't re-render unnecessarily withReact.memo.
Next: Next.js — React for production with SSR, SSG, and full-stack capabilities.