Custom Hooks: Extracting Reusable Logic
Move stateful logic into custom use* functions to share behaviour across components without duplicating code or lifting state.
Custom Hooks: Extracting Reusable Logic is a free Frontend Academy lesson on CoddyKit — lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Frontend Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
What Are Custom Hooks?
A custom hook is a JavaScript function whose name starts with use and that calls other hooks internally. Custom hooks let you extract stateful logic so multiple components can share it without copy-pasting.
Extracting Logic into a Custom Hook
Identify logic that's duplicated across components (fetch + loading + error, window size, local storage) and move it into a custom hook.
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handler = () => setWidth(window.innerWidth);
window.addEventListener('resize', handler);
return () => window.removeEventListener('resize', handler);
}, []);
return width;
}
// Usage:
const width = useWindowWidth();
const isMobile = width < 768;useFetch — Data Fetching Hook
A reusable data fetching hook that any component can use to load data from a URL.
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const controller = new AbortController();
async function load() {
try {
const res = await fetch(url, { signal: controller.signal });
if (!res.ok) throw new Error(`${res.status}`);
setData(await res.json());
} catch (err) {
if ((err as DOMException).name !== 'AbortError')
setError((err as Error).message);
} finally { setLoading(false); }
}
load();
return () => controller.abort();
}, [url]);
return { data, loading, error };
}useLocalStorage Hook
A custom hook that syncs a value to localStorage and stays in sync across tabs via the storage event.
function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
});
const set = useCallback((newValue: T) => {
setValue(newValue);
localStorage.setItem(key, JSON.stringify(newValue));
}, [key]);
return [value, set] as const;
}Custom Hooks Share Logic, Not State
Each component that calls a custom hook gets its own independent state. Hooks are not singletons — they're function calls that create isolated state instances.
Naming Conventions
Custom hook names must start with use. This is enforced by ESLint's react-hooks linting rules, which ensure hooks are called at the top level and in the right order.
Returning Multiple Values
Custom hooks typically return an object (named values) or a tuple (like useState). Tuples allow destructuring with custom names; objects are self-documenting.
// Tuple (like useState):
const [value, setValue] = useLocalStorage('key', 0);
// Object (more descriptive):
const { data, loading, error, refetch } = useFetch('/api/users');useDebounce Hook
Debounce a value: only update it after the user stops typing for a specified delay.
function useDebounce<T>(value: T, delay: number): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}
// Usage:
const debouncedSearch = useDebounce(searchTerm, 300);
useEffect(() => { search(debouncedSearch); }, [debouncedSearch]);useMediaQuery Hook
Wraps window.matchMedia in a hook to reactively read CSS media query results.
function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(() => window.matchMedia(query).matches);
useEffect(() => {
const mq = window.matchMedia(query);
const handler = (e: MediaQueryListEvent) => setMatches(e.matches);
mq.addEventListener('change', handler);
return () => mq.removeEventListener('change', handler);
}, [query]);
return matches;
}Testing Custom Hooks
Use @testing-library/react's renderHook() utility to test custom hooks in isolation without wrapping them in a component.
import { renderHook, act } from '@testing-library/react';
test('useCounter increments', () => {
const { result } = renderHook(() => useCounter(0));
expect(result.current.count).toBe(0);
act(() => result.current.increment());
expect(result.current.count).toBe(1);
});Published Hook Libraries
Community hook libraries provide hundreds of tested hooks: @uidotdev/usehooks, react-use, usehooks-ts. Before writing a custom hook, check if a well-tested version already exists.
Quick Check
What happens to the state inside a custom hook when it's called by two different components?
Recap: Custom Hooks
Custom hooks extract stateful logic for reuse. Name with use prefix. Each component calling the hook gets independent state. Return objects or tuples. Common patterns: useFetch, useLocalStorage, useDebounce, useMediaQuery. Test with renderHook(). Check community libraries before writing from scratch.
Frequently asked questions
Is the “Custom Hooks: Extracting Reusable Logic” lesson free?
Yes — the full text of “Custom Hooks: Extracting Reusable Logic” is free to read here on the web, and the Frontend Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Frontend Academy course, upgrade to CoddyKit PRO.
What will I learn in “Custom Hooks: Extracting Reusable Logic”?
Move stateful logic into custom use* functions to share behaviour across components without duplicating code or lifting state. You practise Frontend Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start Frontend Academy?
No prior experience is required. Frontend Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Custom Hooks: Extracting Reusable Logic” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this Frontend Academy lesson?
Yes. Every Frontend Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- useContext for Global State
- useReducer for Complex State
- useMemo and useCallback for Performance
- Custom Hooks: Extracting Reusable Logic