CodeMIRAI
0 XP

Custom hooks

3 min read 4 runnable examples Quiz included

A custom hook is a function whose name starts with use and which calls other hooks. It lets components share logic: how state changes, what effects run. Each component that calls it still gets its own state.

useToggle

React (JSX)
import { useState } from "react";

function useToggle(initial = false) {
  const [on, setOn] = useState(initial);
  const toggle = () => setOn((value) => !value);
  return [on, toggle];
}

function App() {
  const [dark, toggleDark] = useToggle(false);
  const [details, toggleDetails] = useToggle(true);

  return (
    <div style={{ padding: 16, borderRadius: 10, background: dark ? "#14172a" : "#f3f4f8", color: dark ? "#e9ecf5" : "#14172a" }}>
      <button onClick={toggleDark}>{dark ? "Light" : "Dark"} mode</button>{" "}
      <button onClick={toggleDetails}>{details ? "Hide" : "Show"} details</button>
      {details && <p>Each call to useToggle has its own, independent state.</p>}
    </div>
  );
}

useToggle packages a pattern, a boolean plus a function that flips it, and gives it a name. The component reads like a description of what it does.

Each call gets its own state

React (JSX)
import { useState } from "react";

function useCounter(start = 0, step = 1) {
  const [count, setCount] = useState(start);
  return {
    count,
    up: () => setCount((c) => c + step),
    down: () => setCount((c) => c - step),
    reset: () => setCount(start),
  };
}

function Counter({ label, start, step }) {
  const { count, up, down, reset } = useCounter(start, step);
  return (
    <p>
      {label}: <strong>{count}</strong>{" "}
      <button onClick={down}>-{step}</button> <button onClick={up}>+{step}</button> <button onClick={reset}>reset</button>
    </p>
  );
}

function App() {
  return (
    <div>
      <Counter label="Cups of tea" start={0} step={1} />
      <Counter label="Minutes of practice" start={30} step={15} />
    </div>
  );
}

The two counters share the hook's code but not its state. A hook can return whatever is convenient: an array like useState, or an object with named parts, as here.

useDebounce: wait until typing stops

React (JSX)
import { useEffect, useState } from "react";

function useDebounce(value, delay) {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const id = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(id);       // a new keystroke restarts the wait
  }, [value, delay]);
  return debounced;
}

function App() {
  const [text, setText] = useState("");
  const query = useDebounce(text, 500);
  const [searches, setSearches] = useState(0);

  useEffect(() => {
    if (query) setSearches((n) => n + 1);    // imagine an expensive search here
  }, [query]);

  return (
    <div>
      <input value={text} onChange={(e) => setText(e.target.value)} placeholder="Type quickly..." />
      <p>Typed: {text}</p>
      <p>Searching for: {query}</p>
      <p>Searches run: {searches}</p>
    </div>
  );
}

Type a whole word quickly, and only one search runs, half a second after you stop. Every keystroke cancels the previous timer through the effect's cleanup.

useStoredState: remember a value

React (JSX)
import { useEffect, useState } from "react";

function useStoredState(key, initial) {
  const [value, setValue] = useState(() => {
    try {
      const saved = window.localStorage.getItem(key);
      return saved !== null ? JSON.parse(saved) : initial;
    } catch {
      return initial;            // storage unavailable: private mode, or a sandboxed page
    }
  });

  useEffect(() => {
    try {
      window.localStorage.setItem(key, JSON.stringify(value));
    } catch {
      // nothing to do: the value still works for this visit
    }
  }, [key, value]);

  return [value, setValue];
}

function App() {
  const [name, setName] = useStoredState("cm-demo-name", "");
  return (
    <div>
      <input value={name} onChange={(e) => setName(e.target.value)} placeholder="Your name" />
      <p>{name ? `Hi, ${name}!` : "Type your name."}</p>
    </div>
  );
}

On a normal web page, the name survives a reload, because the hook saves it to localStorage. This preview runs in a locked-down sandbox where storage is blocked, so the hook quietly falls back to ordinary state, which is exactly why the try blocks are there. Passing a function to useState means the saved value is read only once, on the first render.

The rules of hooks

  1. Call hooks only at the top level of a component or custom hook: never inside an if, a loop, or a nested function.
  2. Call hooks only from React functions: components and other hooks.
React (JSX)
function Profile({ user }) {
  if (!user) return null;
  const [tab, setTab] = useState("posts");   // wrong: skipped whenever there's no user
}

React tells hooks apart by the order they're called in during each render. A hook inside an if would shift the order whenever the condition changed, mixing up one hook's state with another's. Put the condition after the hooks instead.

Finished this lesson?Mark it complete to earn +20 XP and move your progress on.