🧩 Functional and Class Components
Every React interface is a tree of components — small, reusable functions that describe a piece of UI. React gives you two ways to write them. This lesson shows you both, teaches you the modern one properly, and gives you the vocabulary to read the older one when you meet it in real code.
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Explain what a component is and why React is built from them
- Write a function component that accepts props and manages state with the
useStateHook - Read a class component and recognize its
render()method,this.state, and lifecycle methods - Map class lifecycle methods to their
useEffectequivalents - Explain why error boundaries are the one thing that still needs a class today
Estimated Time: 30–40 minutes • Difficulty: Beginner–Intermediate
Hands-on: Convert a stateful class component into a modern function component with Hooks.
In This Lesson
Components: The Building Blocks
A component is a reusable, self-contained piece of a user interface. A button is a component. So is a search bar, a comment, a whole sidebar, or an entire page. In React you build a screen by nesting small components inside bigger ones until the whole interface is described as a tree.
💡 Analogy — LEGO bricks. A single LEGO brick is simple and does one thing. But snap bricks together and you can build a spaceship. React components work the same way: each one is small and focused, yet composing them lets you build interfaces of any complexity. And like LEGO, the same brick can be reused in a dozen different models.
Components give you three big wins:
- Reusability — write a
Buttononce, use it everywhere. - Isolation — each component owns its own markup, styling logic, and state, so a bug in one rarely breaks another.
- Composability — complex UIs are just simple components arranged in a tree.
Every box above is a component. Data flows down the tree; each component decides how to render the piece of UI it's responsible for.
Two Ways to Write a Component
React has always supported two syntaxes for defining a component. Understanding both matters: you'll write function components in new code, but you'll read class components in older codebases and tutorials.
Here is the same greeting written both ways so you can see the shapes side by side:
As a function component
function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
}
As a class component
import { Component } from 'react';
class Greeting extends Component {
render() {
return <h1>Hello, {this.props.name}!</h1>;
}
}
💡 Which should you learn first?
Function components. Since Hooks landed in React 16.8 (2019), the React team recommends them for all new code, and the official react.dev docs are written entirely around them. We'll spend most of this lesson on functions and treat classes as "code you can read."
Function Components in Depth
A function component is exactly what it sounds like: a JavaScript function that returns JSX. React calls your function whenever it needs to render, passes in the props as the first argument, and puts whatever JSX you return on the screen.
Destructuring props
Rather than reaching into props.name repeatedly, destructure the props right in the parameter list. It reads cleanly and documents what the component expects:
function ProfileCard({ name, title, avatar }) {
return (
<div className="profile-card">
<img src={avatar} alt={name} />
<h2>{name}</h2>
<p>{title}</p>
</div>
);
}
Default values
Give props sensible defaults with standard JavaScript default parameters — no special React API required:
function Button({ label = 'Click me', variant = 'primary', onClick }) {
return (
<button className={`btn btn-${variant}`} onClick={onClick}>
{label}
</button>
);
}
Adding state with useState
A component often needs to remember something between renders — how many times a button was clicked, whether a menu is open, what the user typed. That memory is called state, and in a function component you add it with the useState Hook.
import { useState } from 'react';
function Counter() {
// useState returns the current value and a setter function
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
📖 Reading useState
const [count, setCount] = useState(0) does three things: sets the initial value to 0, gives you count (the current value), and gives you setCount (the function that updates it). Calling setCount tells React to re-render the component with the new value.
Never assign to a state variable directly (count = 5 does nothing useful) — always go through the setter so React knows to re-render.
⚠️ Rules of Hooks
Hooks like useState must be called at the top level of your component — never inside a loop, condition, or nested function, and only from React function components (or other Hooks). React relies on the call order staying the same on every render. Names conventionally start with use.
Class Components (the Legacy Way)
Before Hooks, the only way to have state or run code at specific moments was a class. A class component extends React.Component, keeps its UI in a render() method, and stores state in this.state.
import { Component } from 'react';
class Counter extends Component {
constructor(props) {
super(props);
this.state = { count: 0 }; // state lives here
this.handleClick = this.handleClick.bind(this); // bind 'this'
}
handleClick() {
// Use the updater form so React batches correctly
this.setState((prev) => ({ count: prev.count + 1 }));
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={this.handleClick}>Click me</button>
</div>
);
}
}
Notice the friction that Hooks removed:
- State must be initialized in a
constructorthat callssuper(props). - Event handlers must be bound to
this(or written as class fields) orthis.stateisundefinedat call time. - Updates go through
this.setState(), which merges into existing state rather than replacing it.
💡 Why classes felt awkward
The this keyword in JavaScript is notoriously slippery, and forgetting to bind a method was one of the most common beginner bugs in React. Function components sidestep this entirely, which is a big part of why the community embraced them.
Lifecycle Methods vs. Hooks
Components go through a lifecycle: they mount (appear), update (re-render when data changes), and unmount (get removed). Class components expose named methods for these moments. Function components handle all of them with a single Hook — useEffect.
| Class lifecycle method | Function-component equivalent |
|---|---|
constructor (set initial state) | useState(initialValue) |
componentDidMount | useEffect(() => {...}, []) |
componentDidUpdate | useEffect(() => {...}, [dep]) |
componentWillUnmount | useEffect(() => { return cleanup; }, []) |
shouldComponentUpdate | React.memo, useMemo |
componentDidCatch | no Hook — needs a class |
Here is a data-fetching component both ways. First the class, with logic split across two lifecycle methods:
class UserProfile extends Component {
state = { user: null, loading: true, error: null };
componentDidMount() {
this.load();
}
componentDidUpdate(prevProps) {
if (prevProps.userId !== this.props.userId) this.load();
}
async load() {
this.setState({ loading: true });
try {
const res = await fetch(`/api/users/${this.props.userId}`);
const user = await res.json();
this.setState({ user, loading: false, error: null });
} catch (err) {
this.setState({ error: err.message, loading: false });
}
}
render() {
const { user, loading, error } = this.state;
if (loading) return <p>Loading…</p>;
if (error) return <p>Error: {error}</p>;
return <h2>{user.name}</h2>;
}
}
Now the function version. The mount-and-update logic collapses into one useEffect whose dependency array ([userId]) says "re-run whenever userId changes":
import { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
async function load() {
setLoading(true);
try {
const res = await fetch(`/api/users/${userId}`);
const data = await res.json();
if (!cancelled) { setUser(data); setError(null); }
} catch (err) {
if (!cancelled) setError(err.message);
} finally {
if (!cancelled) setLoading(false);
}
}
load();
return () => { cancelled = true; }; // cleanup runs on unmount / re-run
}, [userId]);
if (loading) return <p>Loading…</p>;
if (error) return <p>Error: {error}</p>;
return <h2>{user.name}</h2>;
}
✅ What improved
Related logic that was scattered across componentDidMount and componentDidUpdate now lives together in one effect. The returned cleanup function guards against setting state on an unmounted component — a common real-world race condition.
When You Still Need a Class
There is one job Hooks cannot do yet: error boundaries. An error boundary catches JavaScript errors thrown while rendering a part of the tree and shows a fallback UI instead of a blank white screen. It requires the class-only methods getDerivedStateFromError and componentDidCatch.
import { Component } from 'react';
class ErrorBoundary extends Component {
state = { hasError: false };
static getDerivedStateFromError() {
// Render the fallback on the next render
return { hasError: true };
}
componentDidCatch(error, info) {
// Send to your error-reporting service here
console.error('Caught by boundary:', error, info);
}
render() {
if (this.state.hasError) {
return this.props.fallback ?? <p>Something went wrong.</p>;
}
return this.props.children;
}
}
// Usage — wrap any subtree you want to protect
function App() {
return (
<ErrorBoundary fallback={<p>The chart failed to load.</p>}>
<RevenueChart />
</ErrorBoundary>
);
}
In practice most teams write a single ErrorBoundary class once (or install one from a library like react-error-boundary) and never write another class again. Everything inside it can be function components.
Hands-on: Convert a Component
🏋️ Modernize a class component
Objective: Rewrite this stateful class component as a function component using Hooks. It's a toggle button that flips between ON and OFF.
import { Component } from 'react';
class ToggleButton extends Component {
constructor(props) {
super(props);
this.state = { isOn: false };
this.toggle = this.toggle.bind(this);
}
toggle() {
this.setState((prev) => ({ isOn: !prev.isOn }));
}
render() {
return (
<button
className={this.state.isOn ? 'btn-on' : 'btn-off'}
onClick={this.toggle}
>
{this.state.isOn ? 'ON' : 'OFF'}
</button>
);
}
}
💡 Hint
Replace this.state = { isOn: false } with a useState(false) Hook. The toggle handler becomes a plain function inside the component — no bind, no this. Use the updater form setIsOn(prev => !prev) so it always flips the latest value.
✅ Solution
import { useState } from 'react';
function ToggleButton() {
const [isOn, setIsOn] = useState(false);
const toggle = () => setIsOn((prev) => !prev);
return (
<button className={isOn ? 'btn-on' : 'btn-off'} onClick={toggle}>
{isOn ? 'ON' : 'OFF'}
</button>
);
}
Roughly 25 lines became 11, the constructor and bind disappeared, and there's no this to trip over. That compression is exactly why the ecosystem moved to Hooks.
🚀 Stretch goal
Add a prop initialOn so the parent can decide the starting state: <ToggleButton initialOn />. Pass it as the initial value to useState.
Best Practices
✅ Do
- Default to function components for all new code.
- Name components in PascalCase (
UserProfile). React treats lowercase tags as HTML elements, so a lowercase component won't render. - Keep components small and focused — one clear responsibility each.
- Match the filename to the component (
UserProfile.jsx). - Use the updater form of a setter (
setCount(c => c + 1)) when the new value depends on the old.
⚠️ Don't
- Don't reach for a class unless you specifically need an error boundary.
- Don't mutate state directly — always go through the setter.
- Don't call Hooks conditionally or inside loops; keep them at the top level.
- Don't forget the
useEffectdependency array — omitting it re-runs the effect on every render.
Summary & Quiz
🎉 Key Takeaways
- Components are reusable, self-contained pieces of UI; React apps are trees of them.
- Function components are the modern default: a function that takes props and returns JSX, using
useStateanduseEffectfor state and side effects. - Class components use
render(),this.state,this.setState(), and named lifecycle methods — you should be able to read them. - Lifecycle methods map cleanly onto
useEffect; one effect can replacecomponentDidMount+componentDidUpdate. - Error boundaries are the one feature that still requires a class today.
🎯 Quick Quiz
Question 1: How does a modern function component add state?
Question 2: Which class lifecycle method is replaced by useEffect(() => {...}, [])?
Question 3: What is the one common feature that still requires a class component?
📚 Further Reading
🚀 What's Next?
Now that you can write a component, the next question is how to feed it data. In Props and Data Flow we'll dig into props — how parents pass information down to children and how React's one-way data flow keeps apps predictable.
🎉 Nice work!
You can now read any React component and know exactly what you're looking at.