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.
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.
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.
useFetchLet’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>; }
Refactor your existing movie-browser search logic.
useFetch hook that accepts a url.useFetch into your App.jsx.useEffect and useState calls in App.jsx with the return values from your new hook.use prefix: React relies on the use naming convention to identify hooks. If you name your function fetchData instead of useFetch, React will throw an error when you try to call other hooks inside it.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).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.
Learn why side effects like API calls belong in the useEffect hook. Distinguish between rendering and side effects to build predictable React applications.
Read moreLearn to master scalability through expert refactoring techniques. We'll extract reusable UI, optimize folder structures, and decouple logic for cleaner code.
Extracting Custom Hooks