Extracting Custom Hooks: A Guide to React Reusability
Learn to create custom hooks in React to abstract complex data-fetching logic. Improve your code reusability and simplify your components by building a useFetch.
Previously in this course, we explored Folder Structure Best Practices to organize our project. Now that our movie-browser app is growing, you've likely noticed that our data-fetching logic—the useState and useEffect patterns we mastered in Handling Loading States in React and Managing Errors—is starting to clutter our components.
It’s time to solve this by creating custom hooks.
The Problem: Logic Bloat
When you fetch data in multiple components, you find yourself rewriting the same boilerplate: declaring data, loading, and error states, plus the useEffect block to trigger the fetch.
Not only does this lead to repetitive code, but it also makes your components harder to read. If you decide to change how you handle errors or add a loading timeout, you’d have to hunt down every instance of that logic throughout your app. Abstraction allows us to pull this shared behavior into a single, reusable function.
From First Principles: What is a Custom Hook?
A custom hook is simply a JavaScript function that starts with the word use and calls other React hooks.
Think of it as a "logic container." It doesn't return JSX; instead, it returns the values or functions that your component needs to render its UI. By moving our data-fetching logic into a custom hook, we treat that logic as a black box: the component asks for data, and the hook provides it.
Worked Example: Creating useFetch
Let’s move our movie-fetching logic into a dedicated file. Create a new file at src/hooks/useFetch.js.
JAVASCRIPTimport { useState, useEffect } from CE9178">'react'; export function useFetch(url) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const fetchData = async () => { try { setLoading(true); const response = await fetch(url); if (!response.ok) throw new Error(CE9178">'Failed to fetch'); const json = await response.json(); setData(json); } catch (err) { setError(err.message); } finally { setLoading(false); } }; fetchData(); }, [url]); return { data, loading, error }; }
Now, look at how much cleaner your main component becomes. Instead of ten lines of state and effects, you have one:
JAVASCRIPTimport { useFetch } from CE9178">'./hooks/useFetch'; function MovieList() { const { data, loading, error } = useFetch(CE9178">'https://api.example.com/movies'); if (loading) return <p>Loading...</p>; if (error) return <p>Error: {error}</p>; return <ul>{data.map(movie => <li key={movie.id}>{movie.title}</li>)}</ul>; }
Hands-on Exercise
Refactor your existing movie-browser search logic.
- Create a
useFetchhook that accepts aurl. - Import
useFetchinto yourApp.jsx. - Replace your manual
useEffectanduseStatecalls inApp.jsxwith the return values from your new hook. - Ensure the UI still renders correctly.
Common Pitfalls
- Forgetting the
useprefix: React relies on theusenaming convention to identify hooks. If you name your functionfetchDatainstead ofuseFetch, React will throw an error when you try to call other hooks inside it. - Over-abstracting: Don't turn every single line of logic into a hook. Only extract code that you actually reuse in at least two places, or logic that is so complex it obscures the purpose of your component.
- Missing Dependencies: Just like in a standard
useEffect, if your custom hook uses variables inside its effect, make sure those variables are included in the dependency array (or passed as arguments to the hook).
Recap
Custom hooks are the primary tool for reusability in modern React. By extracting stateful logic, you keep your components focused on the UI, while your hooks handle the "how" of data management. This separation of concerns makes your codebase significantly easier to test and maintain.
Up next: We'll tackle Prop Drilling and Context API to share data across our component tree without passing props through every single level.
Work with me

Next.js Full-Stack Web App Development
A fast, SEO-ready full-stack web app built with Next.js 16 — from idea to deployed product, by an engineer who ships to production.

Headless WordPress + Next.js Frontend Development
Keep WordPress for content, get a lightning-fast Next.js frontend. The best of both worlds — familiar editing, modern speed.