A custom hook is just a JavaScript function whose name starts with use and that can call other hooks inside it. Custom hooks let you extract and reuse stateful logic between components without repeating code.
Rules of hooks (apply to custom hooks too)
- Only call hooks at the top level — never inside loops, conditions, or nested functions.
- Only call hooks from React function components or from other custom hooks.
Example: useFetch
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 => {
if (!res.ok) throw new Error('Request failed');
return res.json();
})
.then(json => { if (!ignore) { setData(json); setLoading(false); } })
.catch(err => { if (!ignore) { setError(err); setLoading(false); } });
return () => { ignore = true; };
}, [url]);
return { data, loading, error };
}
// usage — the component doesn't need to know how fetching works
function StudentList() {
const { data: students, loading, error } = useFetch('/api/students');
if (loading) return <p>Loading...</p>;
if (error) return <p>Something went wrong.</p>;
return <ul>{students.map(s => <li key={s.id}>{s.name}</li>)}</ul>;
}
Example: useLocalStorage
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
const saved = localStorage.getItem(key);
return saved !== null ? JSON.parse(saved) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
// usage
function Settings() {
const [darkMode, setDarkMode] = useLocalStorage('darkMode', false);
return (
<label>
<input type="checkbox" checked={darkMode} onChange={e => setDarkMode(e.target.checked)} />
Dark mode
</label>
);
}
When to extract a custom hook
- The same
useState+useEffectcombination shows up in two or more components. - A piece of logic (form validation, window size tracking, debouncing, media queries) is complex enough to deserve its own name and tests.
- You want to hide an implementation detail (e.g. which API library is used) behind a simple interface.