The Fetching Data lesson's loading/error/data pattern is useful — and also something you'd end up retyping in every component that fetches anything. A custom hook extracts that logic into a reusable function, exactly the same reason you'd extract any repeated logic into a regular function.
The only real rule: its name starts with use. That's what lets React (and the linter) verify the Rules of Hooks are being followed — hooks can only be called at the top level of a component or another hook, never inside a condition, loop, or nested function.
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let ignore = false;
setLoading(true);
fetch(url)
.then((res) => res.json())
.then((json) => {
if (!ignore) setData(json);
})
.catch((err) => {
if (!ignore) setError(err.message);
})
.finally(() => {
if (!ignore) setLoading(false);
});
return () => {
ignore = true;
};
}, [url]);
return { data, loading, error };
}This is line-for-line the same logic from the Fetching Data lesson — just wrapped in a function of its own.
function UserProfile({ userId }) {
const { data: user, loading, error } = useFetch(`/api/users/${userId}`);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return <h1>{user.name}</h1>;
}
function PostList() {
const { data: posts, loading } = useFetch('/api/posts'); // same hook, reused
if (loading) return <p>Loading...</p>;
return <ul>{posts.map((p) => <li key={p.id}>{p.title}</li>)}</ul>;
}Both components get their own independent call to useFetch — same as two useState calls never sharing state, calling a custom hook twice creates two completely separate instances of its internal state.
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = () => setValue((v) => !v);
return [value, toggle];
}
function Menu() {
const [isOpen, toggleOpen] = useToggle();
return <button onClick={toggleOpen}>{isOpen ? 'Close' : 'Open'}</button>;
}Extract on the second use, not the first
Extract a custom hook once the same stateful logic shows up in a second component — not before. A one-off useState call doesn't need its own hook; the win comes specifically from not repeating logic that would otherwise be copy-pasted.