Chapter 08 - Let's Get Classy
Chapter 08 - Let's Get Classy
Hey everyone! Welcome back to Namaste React!
Today we go back in time and learn Class Components! Before React Hooks (2019), ALL React components were written as classes. You'll see them in legacy codebases, and interviewers still ask about lifecycle methods. Plus, Error Boundaries can ONLY be built with class components!
What we will cover:
- Why class components existed
- Class component syntax — constructor, render, state
- Lifecycle methods — complete flow
- Class vs Functional — direct comparisons
- Error Boundaries — only possible with class components
- Converting class to functional components
- Interview Questions
1. Why Class Components Existed
BEFORE HOOKS (React < 16.8 — before Feb 2019):
================================================
Functional components could only accept props and return JSX.
They had NO state, NO lifecycle methods — just pure UI.
// Before hooks — functional components were "dumb":
const Greeting = ({ name }) => <h1>Hello {name}!</h1>;
// That was it. No state, no side effects.
// For REAL features (state, API calls), you HAD to use class components:
class Counter extends React.Component {
state = { count: 0 };
render() {
return (
<div>
<p>{this.state.count}</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
+
</button>
</div>
);
}
}
// React 16.8 (Feb 2019): Hooks were introduced.
// useState, useEffect, etc. gave functional components SUPERPOWERS!
// Now functional components can do EVERYTHING class components could.
// And more simply!
2. Class Component Syntax
import React from "react";
class UserClass extends React.Component {
// CONSTRUCTOR — initialize state and bind methods
constructor(props) {
super(props); // ← REQUIRED! Calls React.Component's constructor
// Passes props to parent so this.props works!
this.state = {
count: 0,
userInfo: {
name: "Loading...",
location: "Loading...",
}
};
// In old React, you'd bind methods here:
// this.handleClick = this.handleClick.bind(this);
}
// RENDER — returns JSX. Called on every render.
// Must be PURE — no side effects!
render() {
const { name, location } = this.state.userInfo;
const { count } = this.state;
return (
<div className="user-card">
<h1>Count: {count}</h1>
<button onClick={() => this.setState({ count: count + 1 })}>
Increment
</button>
<h2>Name: {name}</h2>
<h3>Location: {location}</h3>
<p>Contact: {this.props.contactInfo}</p>
</div>
);
}
// LIFECYCLE METHOD — runs ONCE after first render
async componentDidMount() {
// ✅ BEST place for API calls
const data = await fetch("https://api.github.com/users/akshay-saraswat");
const json = await data.json();
this.setState({
userInfo: {
name: json.name,
location: json.location,
}
});
}
// LIFECYCLE METHOD — runs after every update (not first render)
componentDidUpdate(prevProps, prevState) {
if (this.state.count !== prevState.count) {
console.log("Count changed:", this.state.count);
}
}
// LIFECYCLE METHOD — runs just before component is removed
componentWillUnmount() {
// ✅ BEST place to clean up!
clearInterval(this.timer);
}
}
3. setState in Class Components
// setState MERGES the object with existing state:
this.setState({ count: 1 });
// State was: { count: 0, userInfo: { name: "..." } }
// After: { count: 1, userInfo: { name: "..." } } ← userInfo preserved!
// In hooks, setState REPLACES state (doesn't merge):
const [user, setUser] = useState({ name: "A", age: 25 });
setUser({ name: "B" });
// State after: { name: "B" } ← age is GONE! Need spread: { ...user, name: "B" }
// Functional update form (class component):
this.setState(prevState => ({
count: prevState.count + 1
}));
// Multiple setState calls are BATCHED in class components too:
this.setState({ count: 1 });
this.setState({ name: "Alice" });
// → ONE re-render (React batches them)
// setState is ASYNCHRONOUS!
this.setState({ count: 5 });
console.log(this.state.count); // ⚠️ Still shows OLD value!
// To read state AFTER it updates, use the callback:
this.setState({ count: 5 }, () => {
console.log(this.state.count); // ✅ Shows 5 (new value)
});
4. Complete Lifecycle Flow
MOUNTING (component appears on screen): ======================================== constructor() ↓ (state initialized, props bound) render() ↓ (JSX returned, React creates DOM) DOM updated ↓ componentDidMount() ← ✅ Make API calls here! UPDATING (state or props change): =================================== setState() or new props received ↓ shouldComponentUpdate(nextProps, nextState) ↓ (return true to continue, false to skip re-render) render() ↓ (new JSX, React diffs, updates DOM) componentDidUpdate(prevProps, prevState) ← ✅ Respond to changes UNMOUNTING (component removed): ================================= componentWillUnmount() ← ✅ Clean up timers, subscriptions ERROR HANDLING: ================ componentDidCatch(error, info) ← Only in Error Boundaries! static getDerivedStateFromError(error) ← Only in Error Boundaries!
WHAT LOGS WHEN A PARENT AND CHILD BOTH MOUNT: ============================================== // Parent renders Child. // React batches renders for performance. Console output order: ───────────────────── Parent constructor Parent render Child constructor ← Parent renders first, then child Child render Child componentDidMount ← Child finishes first (bottom-up DOM build) Parent componentDidMount ← Then parent WHY? DOM is built from the INSIDE OUT. Child's DOM must exist before Parent's DOM is "complete". So componentDidMount fires child → parent. This matches how real DOM works!
5. Error Boundaries — Class Components Only!
THE PROBLEM — JavaScript errors crash the whole React tree:
============================================================
// Without Error Boundary:
// One component throws an error → ENTIRE app goes blank!
// Users see a white screen with no explanation!
// ❌ What happens without Error Boundary:
function BrokenComponent() {
throw new Error("Something went wrong!"); // ← Crashes EVERYTHING!
return <div>Content</div>;
}
ERROR BOUNDARY — catches errors and shows fallback UI:
=======================================================
// An Error Boundary MUST be a class component.
// It must implement one or both of:
// - static getDerivedStateFromError(error) → render fallback UI
// - componentDidCatch(error, info) → log the error
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
// Called during render when error is thrown below.
// Return new state to update state.
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
// Called after error is thrown.
// Use for logging (Sentry, DataDog, etc.)
componentDidCatch(error, errorInfo) {
console.error("Error caught:", error, errorInfo);
// logToSentry(error, errorInfo); // log to error tracking service
}
render() {
if (this.state.hasError) {
return (
<div className="error-fallback">
<h2>Something went wrong!</h2>
<p>{this.state.error?.message}</p>
<button onClick={() => this.setState({ hasError: false })}>
Try Again
</button>
</div>
);
}
return this.props.children; // ← Render children normally
}
}
// USAGE — wrap potentially broken parts:
<ErrorBoundary>
<RestaurantMenu /> {/* If this throws, ErrorBoundary catches it */}
</ErrorBoundary>
// Granular — wrap each section independently:
<div>
<ErrorBoundary><Header /></ErrorBoundary>
<ErrorBoundary><Body /></ErrorBoundary> {/* Error here doesn't crash Header! */}
<ErrorBoundary><Footer /></ErrorBoundary>
</div>
WHY ONLY CLASS COMPONENTS?
============================
Error Boundaries need componentDidCatch and getDerivedStateFromError.
These lifecycle methods don't have Hook equivalents.
React has not (as of 2025) introduced useErrorBoundary.
There IS a react-error-boundary library that wraps it in a usable API.
import { ErrorBoundary } from "react-error-boundary";
function FallbackComponent({ error, resetErrorBoundary }) {
return (
<div>
<p>Error: {error.message}</p>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
<ErrorBoundary FallbackComponent={FallbackComponent}>
<RestaurantMenu />
</ErrorBoundary>
6. Class vs Functional — Direct Comparisons
FEATURE CLASS COMPONENT FUNCTIONAL COMPONENT
──────────────────────────────────────────────────────────────────
State this.state + this.setState() useState hook
Mount effect componentDidMount() useEffect(() => {}, [])
Update effect componentDidUpdate() useEffect(() => {}, [dep])
Unmount cleanup componentWillUnmount() useEffect(() => () => clean, [])
Error handling componentDidCatch() No hook (use class or library)
Performance opt shouldComponentUpdate() React.memo + useMemo
Ref access React.createRef() useRef()
Context static contextType = useContext()
Boilerplate LOT of boilerplate Minimal!
Readability Harder (this everywhere) Cleaner
// Same component — class vs functional:
// CLASS:
class Timer extends React.Component {
state = { seconds: 0 };
componentDidMount() {
this.interval = setInterval(() => {
this.setState(prev => ({ seconds: prev.seconds + 1 }));
}, 1000);
}
componentWillUnmount() {
clearInterval(this.interval);
}
render() {
return <h1>{this.state.seconds} seconds</h1>;
}
}
// FUNCTIONAL (same thing, much cleaner):
const Timer = () => {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
return () => clearInterval(interval); // cleanup!
}, []);
return <h1>{seconds} seconds</h1>;
};
Interview Questions
Q: What are the React lifecycle methods?
"React lifecycle has 3 phases. Mounting: constructor (init state), render (return JSX), componentDidMount (API calls). Updating: shouldComponentUpdate (skip re-render?), render (new JSX), componentDidUpdate (respond to changes). Unmounting: componentWillUnmount (cleanup timers/subscriptions). There are also rarely used methods like getDerivedStateFromProps, getSnapshotBeforeUpdate, and getDerivedStateFromError for error boundaries."
Q: What is the best place to make API calls in a class component?
"componentDidMount is the best place for API calls. It runs once after the component is rendered and added to the DOM. The render method must be pure (no side effects), and constructor is not appropriate for async operations. componentDidMount is equivalent to useEffect(() => {}, []) in functional components."
Q: What is an Error Boundary?
"An Error Boundary is a class component that catches JavaScript errors anywhere in its child component tree, logs those errors, and displays a fallback UI instead of crashing the whole app. It must implement getDerivedStateFromError (to render fallback) and optionally componentDidCatch (to log the error). They must be class components because no equivalent hooks exist yet."
Q: Why does super(props) need to be called in a constructor?
"super(props) calls the parent class constructor — React.Component's constructor. This is required to properly initialize the component. Passing props ensures that this.props is available inside the constructor. Without super(props), calling this.props in the constructor would return undefined, even though it works fine in render and other methods."
Key Points to Remember
| Concept | Key Takeaway |
|---|---|
| constructor | Initialize state. Call super(props). NOT for side effects or API calls. |
| render | Returns JSX. Must be PURE — no setState, no API calls. |
| componentDidMount | Runs once after first render. Best place for API calls and subscriptions. |
| componentDidUpdate | Runs after every re-render. Check prevProps/prevState to avoid infinite loops. |
| componentWillUnmount | Runs before removal. Clear timers, cancel fetches, close sockets. |
| setState merges | this.setState({ count: 1 }) merges with existing state. Hooks replace. |
| Error Boundary | Class component catching render errors. Shows fallback UI. Only class-based. |
| getDerivedStateFromError | Static method — return state update to show fallback UI when error occurs. |
| componentDidCatch | Called with error + info — for logging to Sentry/DataDog. |
What's Next?
In Chapter 09, we learn advanced performance optimization — Custom Hooks, useMemo, useCallback, React.memo, and code splitting. We make our app production-fast!
Keep coding, keep learning! See you in the next one!
Post a Comment