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. This is 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, and your progress syncs across the web and the CoddyKit app. The Frontend Academy course includes 4 lessons in total.
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.
Learn HTML with an AI tutor — free
Write and run real code in your browser, get instant help from a 24/7 AI tutor, and pick up where you left off on the web or in the app.
- Courses
- 41
- Lessons
- 163
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. 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. The Frontend Academy course includes 4 lessons in total.
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, so you can start here or from the beginning and move at your own pace. This is lesson 4 of 4.
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