Custom hooks
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
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
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
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
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
- Call hooks only at the top level of a component or custom hook: never inside an
if, a loop, or a nested function. - Call hooks only from React functions: components and other hooks.
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.