Course
React — The Complete Course
Rendering, components, hooks, state architecture, performance, and React interviewing.
Language course · 6 units
Unit 1: Unit 1 — Rendering, JSX & the reactivity model
React renders from state, not from DOM queries. A re-render happens when state/props change; reconciliation diffs the virtual tree. JSX is sugar for React.createElement. Keys must be stable and unique per list.
Teach: the unidirectional data flow
Parent owns state; children receive it via props and notify via callbacks. This is 'lift state up'. A re-render of the parent re-renders children unless memoized — so performance work is about controlling WHEN components re-render.
Exercises
- Controlled counter component — A Counter with buttons that increments/decrements the displayed value, purely from state.
- Lift state: two children share a value — Child A edits a value; Child B displays it with no local duplicate of state.
Practice problems
- Simple Todo list
- Controlled form inputs
Unit 2: Unit 2 — Hooks: useState/useEffect/useMemo/useCallback/useRef
Hooks are how you attach state/side-effects to function components. Rules: call at top level, never conditionally, and the order must be stable. useEffect runs after render (cleanup for subscriptions); useMemo caches computed values; useCallback memoizes callbacks; useRef holds a mutable, stable value.
Teach: the dependency array and stale closures
useEffect(fn, deps) re-runs fn only when deps change. Missing a dep captures a stale value (stale closure). useCallback(fn, deps) keeps fn identity stable while deps unchanged so memoized children skip renders. Every value read inside a hook body that is a prop/state belongs in deps.
Exercises
- Subscribe/unsubscribe pattern — A component that subscribes to a store on mount and unsubscribes on unmount without leaking.
- useMemo a derived value — Filter a large list by a search term; explain when useMemo helps.
- Focus an input on mount with useRef — Autofocus an input when the component mounts.
Practice problems
- Custom useDebounce hook
- Custom useLocalStorage hook
Unit 3: Unit 3 — Performance: memoization, code-splitting, the render budget
Start from 'why does it re-render', not from adding memo everywhere. React.memo skips re-render when props are referentially equal; useCallback/useMemo stabilize props. Code-split with React.lazy + Suspense. Avoid inline object/array props that break memo, measure with Profiler/why-did-you-render.
Teach: referential equality is the whole game
A new {} or [] every render is a new reference, so React.memo can never short-circuit a child that receives it. This is why you useCallback the handler and why you do not define objects inline for memoized children. Correct diagnosis order: what re-renders → why → memo only the hot path.
Exercises
- Stop a child re-render with memo + useCallback — Given an ExpensiveChild receiving a callback, stop it re-rendering on parent state change.
- Lazy-load a route with Suspense — Chunk a heavy page so it is only fetched on navigation.
Practice problems
- Why-did-you-render instrument a list
- Virtualized list (windowing)
Unit 4: Unit 4 — State architecture: Context, reducers, data fetching, testing
Choose state location deliberately: local state for UI-only, context for cross-cutting theme/auth, reducers when transitions are complex, and server state (react-query/SWR) for fetching/caching. Testing: Testing Library fires user events and asserts on accessible output, not on implementation.
Teach: context is not a state-management silver bullet
Context re-renders every consumer when its value changes — fine for theme/auth (rarely changing), costly for high-frequency data. Split contexts, wrap value in useMemo, and keep server state separate from client UI state. Reach for reducer when state transitions are interdependent.
Exercises
- useReducer for a multi-step form — A checkout form (address → payment → review) with next/back transitions and validation.
- Test a counter with Testing Library — Write a test: render Counter, click +, assert the label says 1.
Practice problems
- Context-based theme toggle
- Data fetch with loading/error/empty states
Unit 5: Unit 5 — Components, state & props
Components are functions of (props, state) → UI. Props flow down, state is local, and changes re-render. Master the rendering model: a re-render of a parent re-renders children unless memoized; keys identify list items across re-renders; controlled inputs (value + onChange) vs uncontrolled (ref). Compose via children/props rather than inheritance, and lift state up to the lowest common ancestor that needs it.
Teach: keys and the reconciliation model
React reconciles by element type and key. A stable key (an id, not the array index) lets React reuse the DOM node and preserve local state when a list reorders. Using index as key breaks this when items move — state and focus attach to the wrong row. Controlled inputs: value comes from state and onChange updates it, so the input is always in sync; uncontrolled uses a ref and the DOM owns the value.
Exercises
- Why is index-as-key a bug? — A sortable list uses index as key; toggling a checkbox then sorting reorders wrong rows. Explain and fix.
- Controlled input pattern — Write a controlled text input whose value lives in parent state.
- Lift state between siblings — Two sibling components need to share a value. Where does the state live?
Practice problems
- Build a controlled form with lifted state
- Sortable list with stable keys
- Compose a Card via children prop
Unit 6: Unit 6 — Routing, data fetching & the ecosystem
Real apps need routing, server state, and tooling. React Router: route config, nested routes, loaders, navigation, and code-splitting. Server state (react-query/SWR) caches, dedupes, and invalidates fetches — keep it out of local state. Ecosystem: Vite/Next tooling, styling (Tailwind/CSS modules), testing (Vitest + Testing Library), and the mental model of when to reach for a library vs write it.
Teach: route structure and data loading
Define routes declaratively; nested routes render nested layouts. Load data in a route loader (or a query hook) rather than a raw effect, so loading/error/empty states are first-class. Code-split heavy pages with React.lazy + Suspense so the initial bundle stays small. Keep server state in a query library: it handles caching, dedupe, and refetch so you don't hand-roll it.
Exercises
- Auth-guarded route — Protect /dashboard so unauthenticated users redirect to /login.
- When and how to code-split — A heavy report page slows initial load. What do you do?
- Server state vs local state — A profile page fetches data. Should it live in useState + useEffect or a query library?
Practice problems
- Auth-guarded nested routes
- Code-split a heavy page
- Profile fetch with react-query states