Since their introduction in React 16.8, hooks have revolutionized the way developers write functional components. But with so many available, from useState to useDeferredValue, knowing when and how to use each can get confusing.

This guide breaks down the most commonly used React hooks, compares them side-by-side, and outlines practical use cases and gotchas you should avoid.

Most common React hooks at a glance

Hook Purpose Common Use Case Common Gotchas
useState Add local state to components UI toggles, form fields Re-renders even for same value
useEffect Perform side effects Fetch data, subscribe to events Infinite loops if deps are wrong
useContext Consume context Theme, auth, config Triggers re-renders on context change
useRef Persistent mutable value DOM refs, timers, caching Won’t trigger re-render
useCallback Memoize functions Stable callbacks to children Often overused
useMemo Memoize computed values Derived data, filters Use only for expensive calculations
useReducer Manage complex state Forms, state machines Too heavy for simple needs
useLayoutEffect Synchronous side effects Measuring layout Blocks rendering
useImperativeHandle Customize refs Expose custom API Often overcomplicated
useDeferredValue Defer updates Smooth search/filtering Use for fine-grained perf tuning
useDebugValue Custom label in React DevTools Debugging custom hooks Only visible in DevTools

When to use each and why

useState

Purpose Local state in function components.
Considerations - Even setting the same value re-renders the component.
  - Updating state is async; changes are batched.
import { useState } from 'react';

function Counter() {
  // Declare a state variable 'count' initialized to 0
  // 'setCount' is the function to update 'count'
  const [count, setCount] = useState(0);

  // Render a button that displays the current count
  // On click, increment the count by 1 using setCount
  return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}

useEffect

Purpose Perform side effects after rendering.
Considerations - Runs after paint, not blocking UI.
  - Clean up side effects with a return function.
  - Dependencies are critical to prevent loops or stale data
import { useState, useEffect } from 'react';

function Timer() {
  // Declare a state variable 'seconds' initialized to 0
  // 'setSeconds' is the function to update 'seconds'
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    // Set up an interval that increments 'seconds' every 1000ms (1 second)
    // Using functional update form to always get the latest state value
    const interval = setInterval(() => setSeconds(s => s + 1), 1000);

    // Cleanup function to clear the interval when the component unmounts
    // This prevents memory leaks and stops the timer
    return () => clearInterval(interval);
  }, []); // Empty dependency array means this effect runs only once on mount

  // Render the elapsed time in seconds
  return <p>Time: {seconds}s</p>;
}

useContext

Purpose Access context value from nearest provider.
Considerations - Re-renders on context value change.
  - Avoid deep updates unless necessary.
import { createContext, useContext } from 'react';

// Create a Context object named ThemeContext.
// The argument 'light' is the default value for the context.
// This value will be used if no Provider wraps a component consuming this context.
const ThemeContext = createContext('light');

function ThemedButton() {
  // useContext is a React hook that subscribes to the nearest ThemeContext
  // provider above in the tree. It returns the current context value.
  // If no Provider is found, it returns the default value ('light').
  const theme = useContext(ThemeContext);

  // Render a button element.
  // - The button's CSS class is set to the current theme ('light' or any
  //   other provided value).
  // - The button text displays the current theme.
  return <button className={theme}>Theme: {theme}</button>;
}

function App() {
  return (
    // Wrap ThemedButton inside ThemeContext.Provider to override the default
    // context value.
    // - The value prop sets the current context value ('dark' here).
    // - All components inside this Provider can access 'dark' as the theme.
    <MyContext.Provider value="dark">
      <ThemedButton />
    </MyContext.Provider>
  );
}

useRef

Purpose Persistent mutable value or DOM reference.
Considerations - Changing ref.current does not cause re-render.
  - Useful for timers, DOM access, or instance tracking.

Example with DOM reference

import { useRef, useEffect } from 'react';

function FocusInput() {
  // Create a ref object that will be attached to the input element below
  // useRef returns a mutable object with a `.current` property
  // Initially, `.current` is set to null because the element is not rendered yet
  const inputRef = useRef(null);

  // useEffect hook runs side effects after the component has rendered
  // The empty dependency array means this effect runs only once, right after the
  // first render
  useEffect(() => {
    // Access the current value of the ref, which points to the input DOM element
    // Call the focus() method to set keyboard focus on this input element
    if (inputRef.current) {
      inputRef.current.focus();
    }
  }, []); // Empty array ensures effect runs only on mount

  // Render an input element with the ref attached
  // The placeholder attribute provides user-friendly text when input is empty
  return <input ref={inputRef} placeholder="Focus on load" />;
}

Example with presistent mutable value

import { useRef } from 'react';

function Counter() {
  // Initialize a ref object with the initial value 0.
  // Unlike state, updating this value does NOT cause a re-render.
  // This ref is a container whose `.current` property can be mutated.
  const countRef = useRef(0);

  // Function to increment the count stored in the ref
  const increment = () => {
    // Update the current value of the ref by adding 1
    countRef.current += 1;

    // Log the updated count to the console
    // Note: This update won't trigger a re-render, so the UI won't
    // update automatically
    console.log('Current count:', countRef.current);
  };

  return (
    <div>
      {/* Button that calls increment on click */}
      <button onClick={increment}>Increment Count</button>

      {/* Inform the user to check the console for the count value */}
      <p>Open the console to see the count value update.</p>
    </div>
  );
}

Why not use useState here?

useState causes re-renders: When you update a state with setState, React triggers a re-render to update the UI. If you don’t want the UI to update every time a value changes (like in your counter example where the count is just logged, not displayed), useState is unnecessary overhead.

  • If you have frequent updates that don’t need to reflect immediately in the UI, using useRef avoids excessive re-renders and improves performance.
  • useRef gives you a persistent mutable container that stays the same across renders without triggering them. It’s great for storing values like timers, previous values, or mutable data that don’t directly impact rendering.

When to use useState instead?

  • If you need to display the value in the UI and update the UI when it changes.
  • When changes to the value should cause React to re-render and update DOM.
  • When you want React to manage the lifecycle of that state.

useCallback

Purpose Memoize a function to prevent unnecessary re-creation.
Considerations - Use when passing callbacks to child components.
  - Only re-creates if dependencies change.
  - Adds complexity if overused unnecessarily
import { useCallback, useState } from 'react';

// Button component accepts a prop called `onClick` which is a function
// When the button is clicked, it triggers the onClick function passed
// from props
function Button({ onClick }) {
  return <button onClick={onClick}>Click me</button>;
}

// Counter component maintains a count state and provides a function
// to increment it
function Counter() {
  // Declare a state variable `count` initialized to 0, and a setter
  // function `setCount`
  const [count, setCount] = useState(0);

  // Create a memoized increment function using useCallback hook.
  // This function increments the current count by 1.
  // The empty dependency array `[]` ensures that the same function
  // instance is returned on every render, avoiding unnecessary
  // re-renders of child components that depend on this function.
  const increment = useCallback(() => setCount(c => c + 1), []);

  // Render the Button component and pass the `increment` function a
  // as the onClick handler. When the button inside Button is clicked,
  // it will call `increment` and increase the count.
  return <Button onClick={increment} />;
}

useMemo

Purpose Memoize expensive computations.
Considerations - Avoid overuse; only for expensive calculations.
  - Depends on accurate dependency list.
import { useMemo } from 'react';

function ExpensiveComponent({ input }) {
  // useMemo is used to memoize the result of an expensive calculation.
  // The function inside useMemo will only re-run when the `input` value
  // changes. This helps avoid unnecessary recalculations on every render,
  // improving performance especially when the calculation is heavy.
  const computed = useMemo(() => {
    // Simulate expensive work: initialize a total sum variable.
    let total = 0;
    
    // Perform a large number of iterations to simulate a CPU-heavy task.
    // Each iteration multiplies the current index by the input value and 
    // adds it to total.
    for (let i = 0; i < 1000000; i++) {
      total += i * input;
    }
    
    // Return the computed total after the loop finishes.
    return total;
  }, [input]); // Dependency array: re-run this effect only if `input` changes.

  // Render the computed result inside a paragraph element.
  return <p>Computed: {computed}</p>;
}

useReducer

Purpose Manage complex or multi-step state logic.
Considerations - Good for managing state transitions.
  - Makes logic more testable and maintainable.
  - Can be excessive for basic state needs.
import { useReducer } from 'react'; 

// useReducer is a hook used to manage state logic in a more predictable
// way compared to useState, especially for complex state updates.

function reducer(state, action) {
  // This is the reducer function that determines how the state
  // should change based on the dispatched action.
  // It takes two arguments:
  // 1. state: the current state
  // 2. action: an object describing what change should be made

  switch (action.type) {
    case 'increment':
      // If the action type is 'increment', return a new state object
      // with the count property increased by 1.
      return { count: state.count + 1 };

    default:
      // If the action type is not recognized, return the current
      // state unchanged.
      return state;
  }
}

function Counter() {
  // The Counter component uses the useReducer hook to manage the count state.

  // useReducer returns an array with two elements:
  // 1. state: the current state object ({ count: 0 } initially)
  // 2. dispatch: a function to send actions to the reducer to update state
  const [state, dispatch] = useReducer(reducer, { count: 0 });

  return (
    // Render a button displaying the current count.
    // When the button is clicked, it calls dispatch with an action
    // object { type: 'increment' } which triggers the reducer to update
    // the state by incrementing the count.
    <button onClick={() => dispatch({ type: 'increment' })}>
      Count: {state.count}
    </button>
  );
}

useLayoutEffect

Purpose Side effects before paint.
  Use when you need to measure layout before painting
Considerations - Blocks browser paint; use only if needed.
  - Useful for measuring DOM size.
import { useLayoutEffect, useRef } from 'react';

function Measure() {
  // Create a ref object to hold a reference to the DOM element
  // Initially, ref.current is undefined until the div is mounted
  const ref = useRef();

  // useLayoutEffect runs synchronously after all DOM mutations,
  // but before the browser has painted.
  // This makes it perfect for measuring DOM elements before the user 
  // sees the result.
  useLayoutEffect(() => {
    // Access the current DOM element using ref.current
    // and log its offsetWidth (the layout width including padding and border)
    console.log('Width:', ref.current.offsetWidth);
  }, []); // Empty dependency array means this effect runs only once after
          // the component mounts

  // Render a div element with a ref attached to it
  // When this div mounts, ref.current will point to this DOM node
  return <div ref={ref}>Measure me</div>;
}

Why use useLayoutEffect instead of useEffect for measurements?

useLayoutEffect fires synchronously immediately after the DOM is mutated, but before the browser has painted anything to the screen. This means the effect runs before the user actually sees the UI update, allowing you to perform measurements or DOM reads and make any necessary updates without flicker or visual inconsistencies.

useEffect fires asynchronously after the browser has painted the UI. If you read layout properties inside useEffect, the user might briefly see the unmeasured or unadjusted state because the paint happens first, then the effect runs. This can cause layout shifts or flickering on the screen.

In practical terms: Using useLayoutEffect ensures you read layout values (like offsetWidth, offsetHeight, scroll positions, etc.) before the user sees the page, so any changes based on those measurements (like resizing or animations) can be applied immediately.

If you use useEffect, the browser paints first, then your measurement logic runs. This delay can lead to visual glitches.

// Example using useEffect (causes flicker)
function MeasureWithEffect() {
  const ref = useRef();
  const [width, setWidth] = useState(0);

  useEffect(() => {
    // Runs AFTER the browser paints the initial UI
    // Measuring here means the initial render shows width = 0,
    // then state updates, causing a re-render and visible flicker
    const measuredWidth = ref.current.offsetWidth;
    setWidth(measuredWidth);
  }, []);

  return (
    <div>
      <div ref={ref} style={ { width: '50%'} }>
        Measure me
      </div>
      {/* Initially shows "Measured width: 0px" */}
      {/* Then updates after useEffect runs, causing flicker */}
      <p>Measured width: {width}px</p>
    </div>
  );
}

useImperativeHandle

Purpose Customize instance value exposed via ref.
  Use in combination with forwardRef to expose imperative API
Considerations - Use with forwardRef.
  - Avoid overusing; prefer declarative patterns.
import { useImperativeHandle, useRef, forwardRef } from 'react';

// CustomInput component wrapped with forwardRef to accept a ref from its parent
const CustomInput = forwardRef((props, ref) => {
  // Create a local ref to access the actual input DOM element
  const inputRef = useRef();

  // Customize the instance value that is exposed to the parent when using ref
  useImperativeHandle(ref, () => ({
    // Expose a `focus` method that allows the parent to focus the input element
    focus: () => inputRef.current.focus(),
  }));

  // Render the input element with the local ref attached
  return <input ref={inputRef} />;
});

function Form() {
  // Create a ref that will be passed down to CustomInput
  const ref = useRef();

  return (
    <>
      {/* Pass the ref to CustomInput so the parent can call imperative methods */}
      <CustomInput ref={ref} />
      {/* Button that calls the exposed focus method on the input when clicked */}
      <button onClick={() => ref.current.focus()}>Focus input</button>
    </>
  );
}

useDeferredValue

Purpose Defer rendering non-urgent updates.
  Helps defer expensive UI updates (e.g., filtering large lists)
Considerations - Helps keep UI responsive with heavy updates.
  - Works well with startTransition.
  - Doesn’t delay rendering, just de-prioritizes updates
import { useState, useDeferredValue } from 'react';

// Component that renders the search results
function SearchResults({ query }) {
  // useDeferredValue allows React to defer the update of this value
  // so that more urgent updates (like typing in input) don't get blocked.
  // This helps avoid UI jank when rendering heavy components.
  const deferredQuery = useDeferredValue(query);

  // Simulate a heavy computation by creating 2000 divs,
  // each rendering the deferred query string.
  // Rendering 2000 elements can be slow, so deferring this helps keep input responsive.
  const results = Array(2000).fill().map((_, i) => (
    <div key={i}>{deferredQuery}</div>
  ));

  // Render the list of divs containing the deferred query.
  return <div>{results}</div>;
}

function App() {
  // State to hold the current value of the input box.
  const [input, setInput] = useState('');

  return (
     <div>
      {/* Controlled input field for typing the query */}
      <input
        value={input}
        onChange={e => setInput(e.target.value)}
        placeholder="Type to search..."
      />

      {/* Render the search results, passing the current input as query */}
      <SearchResults query={input} />
    </div>
  );
}

Explanation of key concepts

The useDeferredValue hook defers updating the value passed to it. Instead of immediately updating and potentially causing slow renders, it allows React to prioritize more urgent updates (like typing). The deferred value will update asynchronously when the UI is less busy.

  • Heavy rendering simulation: The 2000 divs simulate a heavy UI operation. Without deferring, every keystroke would cause a big re-render, making typing laggy.
  • Controlled input: The input is controlled by React state (input), so any change triggers a state update, which normally triggers re-render.

By using useDeferredValue, the heavy rendering of search results doesn’t block typing responsiveness, improving user experience.

useDebugValue

Purpose Display debug information for custom hooks in React DevTools.
Considerations - Has no effect on behavior or rendering.
  - Use with custom hooks to expose helpful labels or values.
  - Can format expensive values conditionally.
import { useDebugValue, useState, useEffect } from 'react';

// Custom hook: Tracks the user's online/offline status
function useOnlineStatus() {
  // Initialize state with the browser's current online status
  const [isOnline, setIsOnline] = useState(navigator.onLine);

  useEffect(() => {
    // Define a handler to update state when online status changes
    const updateStatus = () => setIsOnline(navigator.onLine);

    // Listen for the 'online' and 'offline' events
    window.addEventListener('online', updateStatus);
    window.addEventListener('offline', updateStatus);

    // Cleanup listeners on unmount to prevent memory leaks
    return () => {
      window.removeEventListener('online', updateStatus);
      window.removeEventListener('offline', updateStatus);
    };
  }, []); // Empty dependency array means this effect runs once on mount

  // DevTools helper: shows current hook value in React DevTools
  // Helps with debugging this custom hook during development
  useDebugValue(isOnline ? 'Online' : 'Offline');

  // Return current online status to consuming components
  return isOnline;
}

// Component that uses the custom hook to show connection status
function StatusIndicator() {
  // Get the online status from the custom hook
  const isOnline = useOnlineStatus();

  // Render the status to the user
  return <span>{isOnline ? 'Online' : 'Offline'}</span>;
}

Common Mistakes

  • Using hooks conditionally - always call them in the same order
  • Omitting dependencies in useEffect, useCallback, or useMemo
  • Expecting useRef updates to trigger re-renders
  • Overusing memoization hooks — they can reduce performance in simple apps

Side Effects

In React, a side effect is anything that happens outside the scope of rendering your component. This includes interactions with:

  • the browser (e.g., document, window)
  • network requests (e.g., fetch)
  • timers, subscriptions, or manual DOM manipulation

Common Examples of Side Effects are:

  • Fetching data from an API
  • Subscribing to a WebSocket or event listener
  • Modifying the DOM manually
  • Logging or analytics calls
  • Setting up timers (setTimeout, setInterval)

React’s rendering should be pure. Given the same props and state, it should always return the same UI. Side effects break that purity, so they need to be handled outside of the render cycle, typically in a hook like:

  • useEffect, for most effects
  • useLayoutEffect, when timing is critical
  • useInsertionEffect, for styling-related effects (rare)

Side Effects Inside Render = Bad

// Don't do this inside a component body
fetch('/data').then(...) 

This would fire on every render, which is inefficient and potentially buggy. Instead, use:

useEffect(() => {
  fetch('/data').then(...);
}, []);

This ensures the side effect runs once after the component is mounted.

Summary

Hook State Side Effects Performance DOM Access Advanced
useState
useEffect
useContext
useRef
useCallback
useMemo
useReducer
useLayoutEffect
useImperativeHandle
useDeferredValue
useDebugValue
State The hook helps store or manage component state.
Side Effects The hook is used to perform effects outside of rendering.
Performance The hook is primarily used for performance optimizations.
DOM Access The hook can be used to interact with or reference DOM elements.
Advanced The hook is considered advanced or specialized, often used less frequently or requiring deeper understanding.

Re-render behavior

Hook Causes Re-render? Notes
useState When setState is called with a new value.
useEffect Runs after render; doesn’t trigger re-render on its own.
useContext indirectly Re-renders if the context value from the Provider changes.
useRef Updating ref.current does not trigger a re-render.
useCallback Caches a function; doesn’t trigger a re-render.
useMemo Caches a computed value; doesn’t trigger a re-render.
useReducer When dispatch is called and the state changes.
useLayoutEffect Runs synchronously after DOM updates; doesn’t trigger re-render.
useImperativeHandle Used with forwardRef; does not trigger re-render.
useDeferredValue delayed Triggers a deferred re-render when the input value changes.
useDebugValue Used only for React DevTools; no render impact.

Custom hooks

A custom hook is essentially a reusable function that can call other hooks like useState, useEffect, etc., and encapsulate a specific piece of logic that you might want to use in multiple components.

Custom hooks follow the same rules as built-in hooks:

  • Only call hooks at the top level
  • Only call hooks from React functions
  • Custom hooks must start with ‘use’ so React can detect them

Benefits of custom hooks

  • Code reuse: DRY (Don’t Repeat Yourself)
  • Separation of concerns: Logic is decoupled from UI

When to use a custom hook

Use a custom hook when:

  • You need the same logic (e.g., fetching data, form handling, timers) in multiple components.
  • Your component is getting large and hard to manage due to logic.

Example: useWindowWidth

import { useState, useEffect } from 'react';

// Custom hook to get and track the current window width
function useWindowWidth() {
  // Initialize state with the current window width
  // This will hold the value of the window's inner width
  const [width, setWidth] = useState(window.innerWidth);

  // useEffect runs side effects. Here, we set up and clean up
  // an event listener
  useEffect(() => {
    // Define a function to handle window resize
    // It updates the state with the new window width
    const handleResize = () => setWidth(window.innerWidth);

    // Add the resize event listener to the window
    window.addEventListener('resize', handleResize);

    // Return a cleanup function that removes the event listener
    // This prevents memory leaks when the component unmounts
    return () => window.removeEventListener('resize', handleResize);
  }, []); // Empty dependency array means this effect runs only once on mount

  // Return the current window width to the component that uses this hook
  return width;
}

Usage:

import React from 'react';

// Import the custom hook that tracks the window's width
// This hook will return the current width of the browser window
import useWindowWidth from './useWindowWidth';

function MyComponent() {
  // Call the custom hook useWindowWidth
  // This returns the current window width and re-renders the component
  // whenever it changes
  const width = useWindowWidth();

  // Return a JSX element displaying the current window width
  // The text will automatically update if the window is resized
  return <div>Window width is {width}px</div>;
}

Example 2: useFetch

import { useState, useEffect } from 'react';

// Define a custom hook named useFetch that takes a URL as its input
function useFetch(url) {
  // State to store the fetched data. Initially null until data is loaded.
  const [data, setData] = useState(null);

  // State to track the loading status. Starts as true while the fetch
  // is in progress.
  const [loading, setLoading] = useState(true);

  // useEffect runs a side effect — here, it performs a data fetch from
  // the given URL
  useEffect(() => {
    // Call fetch to retrieve data from the API endpoint
    fetch(url)
      // Convert the response to JSON format
      .then(res => res.json())
      // When data is successfully retrieved, store it in state and set
      // loading to false
      .then(data => {
        setData(data);       // Store the fetched data
        setLoading(false);   // Update loading status
      });
    // This effect will re-run whenever the `url` value changes
  }, [url]);

  // Return an object with the current data and loading status
  // Components using this hook can destructure { data, loading } from it
  return { data, loading };
}

Usage:

const { data, loading } = useFetch('https://api.example.com/posts');