Updated 11 days ago | GitHub

React Custom Hooks

Overview

Custom hooks let you extract and reuse stateful logic in React functional components. This guide covers what they are, why to use them, and how to create one.

What is a Custom Hook?

A custom hook is a JavaScript function that:

  • Starts with use (e.g., useCustomHook).
  • Uses React’s built-in hooks (like useState, useEffect) to manage state or side effects.
  • Returns values or functions for components to use.

It’s a way to share logic, not UI, across components.

Why Create a Custom Hook?

  • Reuse Logic: Avoid duplicating code (e.g., fetching data, managing forms).
  • Cleaner Components: Move complex logic out of components for better readability.
  • Encapsulation: Keep related state and behavior together.

React Custom Hooks

Key Vocabulary:

  • Stateful Logic: Logic that involves managing and manipulating data that can change over time (e.g., form values, counters, etc.). For example, useState is a hook that helps with stateful logic by providing a way to store and update values within a component.
  • Side Effects: Operations that interact with the outside world, such as fetching data or modifying the DOM. In React, side effects are often handled with useEffect to run actions after rendering.
  • Dependency Arrays: In useEffect, the dependency array controls when the effect should run. If any value inside the array changes, the effect runs again. If the array is empty ([]), the effect runs only once when the component mounts.

How to Create a Custom Hook?

1. Move Logic into a Reusable Function

Identify repetitive logic (e.g., fetching data) and extract it into its own function.

2. Name it with use

Prefix the function with use to follow React’s hook convention. This is important because React hooks must start with use to differentiate them from regular functions.

3. Use Built-in Hooks Inside

Leverage useState, useEffect, and other React hooks within the custom hook to manage state or side effects.

4. Return State/Functions

Return the data or functions that components need to access, such as a value, setter, or function.

Examples

Example 1: useFetch - Fetching Data

Here’s a custom hook to fetch data:

// useFetch.js
import { useState, useEffect } from 'react';

function useFetch(url) {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // `ignore` guards against race conditions: if `url` changes (or the
    // component unmounts) before this fetch resolves, the cleanup below
    // flips the flag and we skip the stale setState calls.
    let ignore = false;
    setLoading(true);
    setError(null);

    fetch(url)
      .then(res => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res.json();
      })
      .then(json => {
        if (!ignore) {
          setData(json);
          setLoading(false);
        }
      })
      .catch(err => {
        if (!ignore) {
          setError(err);
          setLoading(false);
        }
      });

    return () => {
      ignore = true;
    };
  }, [url]); // Re-fetch if url changes

  return { data, error, loading };
}

// Usage in a component
function App() {
  const { data, error, loading } = useFetch('https://api.example.com/data');
  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;
  return <p>{data.message}</p>;
}
  • Logic: This custom hook uses the useState hook to track data, error, and loading state. The useEffect hook performs a side effect—fetching data from the provided URL when the component mounts or when the url changes.
  • Race conditions: When url changes mid-flight, an earlier fetch can still resolve after a later one and overwrite the newer data. The ignore flag set inside the effect’s cleanup function is the pattern recommended by react.dev to discard stale responses. For production apps, react.dev suggests reaching for a framework’s built-in data fetching or a library like TanStack Query or SWR rather than rolling this yourself.
  • Vocabulary:
    • Side Effects: In this case, the side effect is fetching data from an external API, which occurs within the useEffect hook.
    • Dependency Array: The dependency array [url] tells React to re-run the fetch operation every time the url changes.

Example 2: useLocalStorage - Syncing State with Local Storage

When to Use: When you want to persist simple state (e.g., a user preference) in local storage.

// useLocalStorage.js
import { useState } from 'react';

function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    try {
      const stored = localStorage.getItem(key);
      return stored !== null ? JSON.parse(stored) : initialValue;
    } catch {
      // Stored value isn't valid JSON (e.g. written by an older version
      // of this hook). Fall back to the initial value.
      return initialValue;
    }
  });

  const setStoredValue = (newValue) => {
    // Match React's setState API: accept a value or an updater function
    // (prev => next). Resolve the updater inside `setValue` so we always
    // see the freshest prev, and so we never JSON.stringify a function
    // (which serializes to `undefined`).
    setValue(prev => {
      const valueToStore =
        typeof newValue === 'function' ? newValue(prev) : newValue;
      try {
        if (valueToStore === undefined) {
          // JSON.stringify(undefined) returns the JS value `undefined`,
          // which setItem would coerce to the string "undefined" — and
          // that would then throw on the next JSON.parse. Drop the key.
          localStorage.removeItem(key);
        } else {
          localStorage.setItem(key, JSON.stringify(valueToStore));
        }
      } catch {
        // Writes can fail in private mode or when over quota; swallow so
        // the in-memory state still updates.
      }
      return valueToStore;
    });
  };

  return [value, setStoredValue];
}

// Usage
function ThemeSwitcher() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  return (
    <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
      Switch to {theme === 'light' ? 'Dark' : 'Light'}
    </button>
  );
}

Watch out: localStorage only stores strings — setItem calls String() on whatever you pass, so an object becomes the literal "[object Object]". Always serialize with JSON.stringify on write and JSON.parse on read. A few more pitfalls worth knowing:

  • Check getItem‘s result against null (the value returned when the key isn’t set) rather than using ||, so legitimate stored values like 0, "", or false aren’t overwritten by initialValue.
  • Wrap JSON.parse in try/catch. The value at key might have been written by an older version of the hook (or another script entirely) and may not be valid JSON — without the guard, the hook throws during render.
  • JSON.stringify(undefined) returns the JS value undefined, which setItem then coerces to the string "undefined" — and that string is invalid JSON, so the next read throws. Handle undefined explicitly by calling removeItem instead.
  • Match React’s setState API: callers expect setStoredValue(prev => next) to work. If you skip the updater check and just JSON.stringify whatever they pass, an updater function serializes to undefined and you silently drop the write — leaving storage out of sync with state.
  • Logic: This hook syncs a state value with local storage. It uses the useState hook with a function that initializes the state from localStorage (parsing the stored JSON) or uses the provided initialValue if no value exists in storage.
  • Vocabulary:
    • Stateful Logic: The useState hook manages the state of the theme (either “light” or “dark”).
    • Side Effects: Storing the state in localStorage is a side effect that happens when the state changes.
    • Dependency Array: There’s no explicit dependency array here, but the side effect (localStorage.setItem) occurs whenever the state changes.

Example 3: useCounter - Managing a Counter

When to Use: When you need a reusable counter (e.g., for likes, scores) in multiple places.

// useCounter.js
import { useState } from 'react';

function useCounter(initialCount = 0) {
  const [count, setCount] = useState(initialCount);

  const increment = () => setCount(prev => prev + 1);
  const decrement = () => setCount(prev => prev - 1);
  const reset = () => setCount(initialCount);

  return { count, increment, decrement, reset };
}

// Usage
function Counter() {
  const { count, increment, decrement, reset } = useCounter(0);
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>+1</button>
      <button onClick={decrement}>-1</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}
  • Logic: This custom hook manages a counter with functions to increment, decrement, and reset the count. It uses useState to store and update the count value.
  • Vocabulary:
    • Stateful Logic: The counter is an example of stateful logic—tracking a value (count) that can change over time.
    • Side Effects: This example doesn’t have side effects but can be extended to do so, such as saving the count in local storage or a backend.
    • Dependency Arrays: No useEffect or dependencies are involved here, but the hook relies solely on state changes to trigger re-renders.

References