React Lesson 13: Custom Hooks
Custom hooks let you extract and reuse stateful logic between components. When you find yourself writing the same useState + useEffect pattern in multiple components, extract it to a custom hook.
Rules of Hooks
// 1. Only call hooks at the TOP LEVEL of a function
// (not inside conditions, loops, or nested functions)
// 2. Only call hooks inside React function components
// or other custom hooks
// 3. Custom hook names MUST start with "use"
// useLocalStorage, useFetch, useForm, useTheme
useFetch Hook
import { useState, useEffect } from "react";
// Custom hook
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetch(url)
.then(r => { if (!r.ok) throw new Error(r.status); return r.json(); })
.then(d => { setData(d); setLoading(false); })
.catch(e => { setError(e.message); setLoading(false); });
}, [url]);
return { data, loading, error };
}
// Use it in any component!
function UserList() {
const { data, loading, error } = useFetch("https://jsonplaceholder.typicode.com/users");
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return <ul>{data.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
function PostList() {
const { data, loading } = useFetch("https://jsonplaceholder.typicode.com/posts");
// same hook, different URL!
useLocalStorage Hook
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
try {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
} catch { return initialValue; }
});
const setStoredValue = (newValue) => {
setValue(newValue);
localStorage.setItem(key, JSON.stringify(newValue));
};
return [value, setStoredValue];
}
// Usage
const [theme, setTheme] = useLocalStorage("theme", "dark");
// Now theme persists across page reloads!
🏋️ Practice Task
Build a useDebounce(value, delay) hook that delays updating a value. Use it in a search component: as user types, only fire the search after they stop typing for 300ms. This prevents an API call on every keystroke.
💡 Hint: In useDebounce: use useEffect + setTimeout. Return the debounced value. In component: const debouncedQuery = useDebounce(query, 300).