Skip to main content

Command Palette

Search for a command to run...

React Hooks Explained: Guide to the Six Hooks You'll Use Most

Updated
13 min read
React Hooks Explained: Guide to the Six Hooks You'll Use Most

Every React developer remembers the first time they saw a component with lifecycle methods, constructor functions, and this.setState(...) calls and thought: "There has to be a better way."

React Hooks are that better way.

Introduced in React 16.8, Hooks changed how developers write React components. Before Hooks, managing state and side effects required class-based components, which were verbose, harder to reuse, and easy to get wrong. Hooks let you do all of that inside simple function components, using clean and readable JavaScript.

Today, Hooks are the standard in modern React development. Whether you're building a personal project or working on a large-scale application, understanding Hooks is non-negotiable.

In this article, we'll walk through six of the most commonly used React Hooks:

  • useState — managing component state
  • useEffect — handling side effects
  • useRef — referencing DOM elements and persisting values
  • useMemo — optimising expensive calculations
  • useContext — sharing data across components
  • useCallback — memoising functions to prevent unnecessary re-renders

1. useState — Giving Your Component a Memory

What is it?

useState is the most fundamental hook in React. It enables a component to remember information, such as whether a button was clicked, what a user inserted into a form, or how many product items are in a cart.

Without state, your component would render the same output every single time, ignoring any user interaction. State is what makes your UI dynamic and responsive.

How it works

import { useState } from 'react';

function Counter() {
  // Declare a state variable called "count", starting at 0
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      {/* Call setCount to update the state */}
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

useState(0) sets the initial value of count to 0. It returns two things: the current value (count) and a function to update it (setCount). Every time setCount is called, React re-renders the component with the new value.

When to use it

Use useState whenever a component needs to track something that can change such as a toggle, a form field value, a counter, a list of items, or a loading status.

Real-world use case

Think of a dark mode toggle. When the user clicks the button, you update a isDarkMode state variable from false to true, and your component re-renders with the appropriate styles applied. Simple, clean, effective.


2. useEffect — Running Code at the Right Time

What is it?

useEffect allows you to create side effects in your components. A "side effect" is any action that extends beyond the component, such as fetching data from an API, updating the page title, setting up a timer, or subscribing to an event.

Without useEffect, you'd have no clean way to say "run this code after the component renders." That's exactly the problem it solves.

How it works

import { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    // This runs after the component renders
    fetch(`https://api.example.com/users/${userId}`)
      .then(response => response.json())
      .then(data => setUser(data));
  }, [userId]); // Re-run this effect whenever userId changes

  if (!user) return <p>Loading...</p>;

  return <h1>Hello, {user.name}!</h1>;
}

The second argument — [userId] — is the dependency array. It tells React when to re-run the effect. If you pass an empty array [], the effect runs only once, after the first render. If you pass a variable like userId, the effect re-runs every time that variable changes.

When to use it

Use useEffect when you need to:

  • Fetch data from an API
  • Set or clear a timer
  • Subscribe or unsubscribe from events
  • Update the browser tab title

Real-world use case

An e-commerce product page fetches product details when the page loads. When the user navigates to a different product (changing the product ID in the URL), useEffect detects the change and fetches the new product's data automatically.


3. useRef — A Sticky Note That Doesn't Cause Re-renders

What is it?

useRef gives you a way to hold a reference to something, usually a DOM element or a value you want to persist between renders and without causing the component to re-render when that value changes.

Consider it as a sticky note attached to your component. You can write on it, read it back, and update it without affecting the component's display.

How it works

Example 1: Accessing a DOM element

import { useRef } from 'react';

function TextInput() {
  // Create a ref to attach to an input element
  const inputRef = useRef(null);

  const focusInput = () => {
    // Directly access and focus the input element
    inputRef.current.focus();
  };

  return (
    <div>
      <input ref={inputRef} type="text" placeholder="Type here..." />
      <button onClick={focusInput}>Focus the input</button>
    </div>
  );
}

Example 2: Storing a value without triggering re-renders

import { useRef, useEffect } from 'react';

function Timer() {
  // Store the interval ID without causing re-renders
  const intervalRef = useRef(null);

  useEffect(() => {
    intervalRef.current = setInterval(() => {
      console.log('Tick');
    }, 1000);

    // Clear the interval when the component unmounts
    return () => clearInterval(intervalRef.current);
  }, []);

  return <p>Timer is running. Check the console.</p>;
}

When to use it

Use useRef when you need to:

  • Directly interact with a DOM element (focus, scroll, measure)
  • Store a value that should persist between renders but shouldn't trigger a re-render when it changes
  • Keep track of a previous state value

Real-world use case

A video player component uses useRef to access the <video> element directly, allowing the play and pause buttons to control playback by calling videoRef.current.play() and videoRef.current.pause().


4. useMemo — Don't Recalculate What You Don't Have To

What is it?

useMemo is a performance optimisation tool. It memoises (caches) the result of an expensive calculation and only recalculates it when the inputs change.

When React re-renders a component, it re-runs all the code inside it. If your component performs a significant computation, such as filtering a large list, sorting thousands of items, or performing sophisticated math, the calculation is executed on every render, even if the result remains unchanged. useMemo prevents wasted work.

How it works

import { useState, useMemo } from 'react';

function ProductList({ products, searchTerm }) {
  // Only re-filter the products when products or searchTerm changes
  const filteredProducts = useMemo(() => {
    console.log('Filtering products...'); // Won't log unless inputs change
    return products.filter(product =>
      product.name.toLowerCase().includes(searchTerm.toLowerCase())
    );
  }, [products, searchTerm]);

  return (
    <ul>
      {filteredProducts.map(product => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  );
}

Without useMemo, the filter would run on every re-render, even if the user had simply clicked a button elsewhere on the page unrelated to the product list. Using useMemo, the result is cached and utilized until products or searchTerm changes.

When to use it

Use useMemo when:

  • You have a calculation that is noticeably slow or processes large datasets
  • The calculation's inputs change infrequently

A word of caution: Don't use useMemo everywhere. It adds complexity and has its own overhead. Use it only when you've actually noticed a performance problem and not as a default habit.

Real-world use case

A data analytics dashboard filters and sorts thousands of rows of data. Without useMemo, scrolling through the page or toggling an unrelated UI element would trigger the entire filter operation again. With useMemo, that work is skipped unless the underlying data or filter criteria change.


5. useContext — Sharing Data Without Passing It Everywhere

What is it?

useContext solves a common problem in React called prop drilling by eliminating the frustrating pattern where you pass data down through multiple layers of components just so a deeply nested child can access it.

With useContext, you can make data available to any component in your tree, at any depth, without threading it through every layer in between.

How it works

It takes two steps: first, you create a context and provide it at a high level; then, any child component can consume it with useContext.

Step 1: Create and provide the context

import { createContext, useState } from 'react';

// Create a context with a default value
export const ThemeContext = createContext('light');

function App() {
  const [theme, setTheme] = useState('light');

  return (
    // Wrap your component tree with the Provider
    // Every child can now access "theme"
    <ThemeContext.Provider value={theme}>
      <Layout />
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        Toggle Theme
      </button>
    </ThemeContext.Provider>
  );
}

Step 2: Consume the context in any child component

import { useContext } from 'react';
import { ThemeContext } from './App';

function Button() {
  // Access the theme directly — no props needed
  const theme = useContext(ThemeContext);

  return (
    <button className={`btn btn-${theme}`}>
      I know the current theme!
    </button>
  );
}

Button can be nested five levels deep inside Layout, and it will still have access to theme without theme ever being passed as a prop to any of those intermediate components.

When to use it

Use useContext for data that is truly global to your application — such as:

  • The currently logged-in user
  • The active theme (light/dark)
  • The preferred language or locale
  • A shopping cart shared across multiple pages

Real-world use case

A UserContext is used in multi-page applications to store information about the logged-in user. Every component, from the top navigation bar with the user's name to a settings page with their preferences, gets this information directly from useContext, without any prop drilling required.


6. useCallback — Keeping Functions Stable Across Renders

What is it?

Every time a React component re-renders, every function defined inside it is recreated from scratch. Most of the time, this is harmless. But when you pass a function down to a child component, that child sees a brand-new function on every render, even if the function does exactly the same thing it did before. This can cause unnecessary re-renders in child components, quietly hurting the performance of your application.

useCallback addresses this issue by memoising a function, which returns the same function instance across renderings unless its dependencies change. Think of it as useMemo, but specifically for functions rather than values.

How it works

Let's first look at the problem without useCallback:

import { useState } from 'react';

function SearchPage() {
  const [query, setQuery] = useState('');
  const [darkMode, setDarkMode] = useState(false);

  // ⚠️ This function is recreated on EVERY render
  // — even when only darkMode changes
  const handleSearch = (searchTerm) => {
    console.log('Searching for:', searchTerm);
    // ...fetch results
  };

  return (
    <div>
      <button onClick={() => setDarkMode(!darkMode)}>Toggle Theme</button>
      {/* SearchBox receives a new handleSearch every render */}
      <SearchBox onSearch={handleSearch} />
    </div>
  );
}

Every time the user toggles the theme, SearchPage re-renders and creates a new handleSearch function. SearchBox receives what looks like a changed prop and re-renders too — even though the search logic didn't change at all.

Now, with useCallback:

import { useState, useCallback } from 'react';

function SearchPage() {
  const [query, setQuery] = useState('');
  const [darkMode, setDarkMode] = useState(false);

  // ✅ This function is only recreated when "query" changes
  const handleSearch = useCallback((searchTerm) => {
    console.log('Searching for:', searchTerm);
    // ...fetch results
  }, [query]); // dependency array — same as useEffect and useMemo

  return (
    <div>
      <button onClick={() => setDarkMode(!darkMode)}>Toggle Theme</button>
      {/* SearchBox now receives the same function reference — no unnecessary re-render */}
      <SearchBox onSearch={handleSearch} />
    </div>
  );
}

By wrapping handleSearch in useCallback, React reuses the same function reference as long as query hasn't changed. Toggling the theme no longer causes SearchBox to re-render unnecessarily.

useCallback vs useMemo — what's the difference?

They're closely related, and it's easy to mix them up:

  • useMemo caches the result of a function — the value it returns.
  • useCallback caches the function itself — the callable reference.

In fact, useCallback(fn, deps) is essentially shorthand for useMemo(() => fn, deps). If you need to memoise a value, use useMemo. If you need to memoise a function, use useCallback.

When to use it

Use useCallback when:

  • You're passing a function as a prop to a child component that is wrapped in React.memo (a tool that tells React to skip re-rendering a child if its props haven't changed)
  • A function is listed as a dependency in a useEffect and you don't want the effect to re-run on every render
  • You're passing callbacks into performance-sensitive components like large lists or data tables

Same caution as useMemo: Don't reach for useCallback by default. It adds its own overhead. Use it when you have a real, observed performance issue and not as a precaution.

Real-world use case

Imagine a large comment feed where each comment has a "Like" button. The parent component manages the list and passes a handleLike function to every Comment component. Without useCallback, every state update in the parent (say, a notification counter ticking up) would recreate handleLike and re-render every single comment card. Wrapping handleLike in useCallback ensures those comment cards stay still unless the function's logic actually needs to change.


Putting It All Together

These six Hooks cover the vast majority of what you'll need in real React development:

Hook Purpose Use When...
useState Manage component state Your component needs to track changing data
useEffect Run side effects You need to fetch data, set timers, or subscribe to events
useRef Reference DOM elements or persist values You need direct DOM access or a value that doesn't trigger re-renders
useMemo Optimise expensive calculations A slow calculation is hurting performance
useContext Share data globally Multiple components need the same data without prop drilling
useCallback Memoise functions You're passing functions to child components and want to prevent unnecessary re-renders

Conclusion

React Hooks transformed how developers write React applications. They made code shorter, easier to read, and far simpler to reuse. Once you get comfortable with useState and useEffect, the others will begin to feel natural very quickly.

The most effective way to properly learn Hooks is to apply them. Begin small: create a counter using useState, retrieve some data with useEffect, and toggle a theme with useContext. When your program expands and speed becomes important, use useMemo and useCallback to keep things fast and efficiently.

These six Hooks are the backbone of modern React development. Master them, and you'll have everything you need to write clean, professional, and performant React code.

Clean, hook-powered React code is within your reach. The only step left is to open your editor and start building.


Happy coding — and remember, every expert was once a beginner who just kept going. 😎