Chapter 11 of 12
Custom hooks let you extract reusable stateful logic from components. Any time you find yourself copying the same useState + useEffect pattern across multiple components, that's a custom hook waiting to be written.
import { useState, useEffect } from "react";
// Convention: name starts with 'use'
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
async function fetchData() {
try {
setLoading(true);
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
if (!cancelled) setData(json);
} catch (err) {
if (!cancelled) setError(err.message);
} finally {
if (!cancelled) setLoading(false);
}
}
fetchData();
return () => { cancelled = true; }; // cleanup
}, [url]);
return { data, loading, error };
}
// Use it in any component
function CourseList() {
const { data, loading, error } = useFetch("/api/courses");
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return (
<ul>
{data.map(course => <li key={course.id}>{course.title}</li>)}
</ul>
);
}function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
try {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
} catch {
return initialValue;
}
});
function set(newValue) {
setValue(newValue);
try {
localStorage.setItem(key, JSON.stringify(newValue));
} catch {}
}
return [value, set];
}
// Use it like useState, but it persists across page reloads
function Settings() {
const [theme, setTheme] = useLocalStorage("theme", "dark");
return (
<button onClick={() => setTheme(theme === "dark" ? "light" : "dark")}>
Theme: {theme}
</button>
);
}