Skip to main content

🧩 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 useState Hook
  • Read a class component and recognize its render() method, this.state, and lifecycle methods
  • Map class lifecycle methods to their useEffect equivalents
  • 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 Button once, 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.
graph TD A[App] --> B[Header] A --> C[Dashboard] A --> D[Footer] C --> E[StatCard] C --> F[UserList] F --> G[UserRow] F --> H[UserRow]

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.

Function component versus class component Two panels compare a function component (a plain function returning JSX, using Hooks) with a class component (an ES6 class with a render method and lifecycle methods). Function Component the modern default Plain JavaScript function Receives props as an argument Returns JSX Uses Hooks for state & effects Class Component legacy, still readable ES6 class extends Component Reads props via this.props render() returns JSX Built-in lifecycle methods
Figure 1 — Both do the same job: accept data, return UI. The syntax and the tools for state differ.

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 constructor that calls super(props).
  • Event handlers must be bound to this (or written as class fields) or this.state is undefined at 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.

flowchart LR A[Mount] --> B[Update] B --> B B --> C[Unmount] A -->|class: componentDidMount| A B -->|class: componentDidUpdate| B C -->|class: componentWillUnmount| C
Class lifecycle methodFunction-component equivalent
constructor (set initial state)useState(initialValue)
componentDidMountuseEffect(() => {...}, [])
componentDidUpdateuseEffect(() => {...}, [dep])
componentWillUnmountuseEffect(() => { return cleanup; }, [])
shouldComponentUpdateReact.memo, useMemo
componentDidCatchno 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 useEffect dependency 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 useState and useEffect for 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 replace componentDidMount + 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.