Custom Hooks in React

A custom Hook is a JavaScript function that packages reusable React logic. It can call built-in Hooks, accept inputs, and return the values or actions a component needs.

Custom Hooks share logic

When several components repeat the same state, Effect, or subscription logic, move that logic into a function whose name starts with use. React treats that naming convention as a signal that the function may call Hooks.

useOnlineStatus hides browser event subscription details behind one meaningful value.
import { useEffect, useState } from "react";

export function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(navigator.onLine);

  useEffect(() => {
    function updateStatus() {
      setIsOnline(navigator.onLine);
    }

    window.addEventListener("online", updateStatus);
    window.addEventListener("offline", updateStatus);

    return () => {
      window.removeEventListener("online", updateStatus);
      window.removeEventListener("offline", updateStatus);
    };
  }, []);

  return isOnline;
}

function SaveButton() {
  const isOnline = useOnlineStatus();

  return (
    <button disabled={!isOnline}>
      {isOnline ? "Save" : "Reconnect to save"}
    </button>
  );
}

A custom Hook shares logic, not one shared state value. Two components that call useOnlineStatus each get an independent state cell and Effect lifecycle, even though they reuse the same function.

Reusable stateful logic

Stateful logic is the behavior around state, such as initializing it, updating it, subscribing to an external source, and cleaning up. A custom Hook lets components reuse that behavior without forcing them to reuse the same markup.

The Hook owns behavior

It can manage state, Effects, refs, reducers, and event-oriented helper functions.

The component owns UI

It decides what JSX to render and how returned values appear to the user.

A reusable form-field Hook returns a controlled input value and the actions that change it.
import { useState } from "react";

function useFormField(initialValue = "") {
  const [value, setValue] = useState(initialValue);

  function onChange(event) {
    setValue(event.target.value);
  }

  function reset() {
    setValue(initialValue);
  }

  return { value, onChange, reset };
}

function ProfileForm() {
  const name = useFormField("Ada");
  const city = useFormField();

  function handleReset() {
    name.reset();
    city.reset();
  }

  return (
    <form>
      <input aria-label="Name" value={name.value} onChange={name.onChange} />
      <input aria-label="City" value={city.value} onChange={city.onChange} />
      <button type="button" onClick={handleReset}>Reset</button>
    </form>
  );
}
  • Extract a concept, not every repeated line.

    A useful custom Hook describes a purpose such as online status, form fields, media queries, or chat connections.

  • Keep pure calculations as regular functions.

    If a function does not call Hooks, it does not need a use prefix. A normal function is simpler and may be called anywhere.

  • Keep ownership explicit.

    State belongs to each component call. To share the same changing value, lift state or place it in context, then let a Hook read that context.

Rules of Hooks

Custom Hooks follow the same rules as components. These rules let React match each Hook call to the correct state, ref, or Effect on every render.

  • Call Hooks at the top level.

    Do not call them inside loops, conditions, nested functions, event handlers, or try/catch blocks. Keep the sequence of Hook calls stable across renders.

  • Call Hooks only from React functions.

    Call them from function components or other custom Hooks, not from ordinary JavaScript functions.

  • Name custom Hooks with use.

    Names such as useOnlineStatus communicate that the Rules of Hooks apply and allow React tooling to check usage.

  • Pass conditions as data.

    Call the Hook every render and put conditional behavior inside it. An enabled option can tell an Effect whether it should connect.

Keep the Hook call unconditional. Let its input control the subscription.
// Incorrect: the Hook call moves in and out of the render sequence.
if (roomId) {
  useChatRoom({ roomId });
}

// Correct: the call order stays stable.
useChatRoom({ roomId, enabled: Boolean(roomId) });

function useChatRoom({ roomId, enabled }) {
  useEffect(() => {
    if (!enabled) return;

    const connection = createConnection(roomId);
    connection.connect();

    return () => connection.disconnect();
  }, [roomId, enabled]);
}

A name that starts with use is a contract. If a function never calls Hooks, prefer a regular name. If it does call Hooks, use the prefix and follow the rules.

Custom Hook API design

The Hook's parameters and return value form an API. Design it around what the caller needs to express, while keeping implementation details inside the Hook.

  • Use a specific, purpose-driven name.

    useOnlineStatus communicates more than useEffectWrapper. Name the user-facing capability, not the built-in Hook used internally.

  • Accept reactive inputs directly.

    Pass values such as roomId and serverUrl so dependencies remain visible. Use an options object when several related inputs make positional arguments unclear.

  • Return the smallest useful interface.

    Return values and named actions the component needs. Keep setters, refs, and subscription machinery private unless callers truly need that control.

  • Keep behavior predictable.

    Document whether the Hook subscribes, fetches, or performs cleanup. Prefer actions such as reset and retry over exposing internal state transitions.

  • Preserve dependency semantics.

    Custom Hooks that accept callbacks may need to include them in an Effect dependency list. Callers should pass stable callbacks only when stability is actually required.

This API exposes a status and a retry action while keeping request state and cleanup inside the Hook.
function useUser(userId) {
  const [attempt, setAttempt] = useState(0);
  const [result, setResult] = useState({
    status: "loading",
    user: null
  });

  useEffect(() => {
    let ignore = false;
    setResult({ status: "loading", user: null });

    fetchUser(userId)
      .then(user => {
        if (!ignore) {
          setResult({ status: "success", user });
        }
      })
      .catch(() => {
        if (!ignore) {
          setResult({ status: "error", user: null });
        }
      });

    return () => {
      ignore = true;
    };
  }, [userId, attempt]);

  function retry() {
    setAttempt(value => value + 1);
  }

  return { ...result, retry };
}

function UserProfile({ userId }) {
  const { status, user, retry } = useUser(userId);

  if (status === "loading") return <p>Loading…</p>;
  if (status === "error") return <button onClick={retry}>Retry</button>;
  return <h3>{user.name}</h3>;
}

The example focuses on API shape. Production data fetching usually also represents errors and may use a framework data API or cache that handles deduplication, server rendering, and race conditions.

Check your understanding

  1. If two components call the same custom Hook, do they share one state value?
    Show answer

    No. Each call receives independent state and Effect lifecycles. The components share the Hook's logic, not its state. Shared state needs a common owner, often reached through props or context.

  2. Can a custom Hook be called only when a prop is true?
    Show answer

    No. Call it unconditionally at the top level so Hook order stays stable. Pass the prop into the Hook and use it to control behavior inside an Effect or calculation.

  3. Should a helper named useSortedItems keep the use prefix if it only calls items.toSorted()?
    Show answer

    No. If it calls no Hooks, make it a regular function such as sortItems. Then it can be called anywhere without implying the Rules of Hooks apply.