Building a Custom React Hook from Scratch
React Hooks have revolutionized how we write components by allowing us to use state and other React features without writing a class. While React provides many built-in hooks like useState and useEffect, creating custom hooks lets you encapsulate reusable logic and share it across your application.In this article, we will walk through building a custom React hook from scratch. We will cover why custom hooks are useful, how to create them, and provide a practical example.
Why Create Custom Hooks?
Custom hooks allow you to extract component logic into reusable functions. This keeps your components clean and promotes code reuse. If you find yourself repeating similar code in multiple components, a custom hook can help you avoid duplication.
For example, you might want a hook that handles fetching data, tracking window size, managing form inputs, or handling authentication state. Custom hooks are just JavaScript functions that use other hooks internally.
The Rules of Hooks
Before we start, remember that hooks must follow the Rules of Hooks:
- Only call hooks at the top level of your function. Do not call hooks inside loops, conditions, or nested functions.
- Only call hooks from React function components or custom hooks.
Following these rules ensures React can correctly track hook state and behavior.
Building Our Custom Hook: useFetch
Let's build a custom hook called useFetch that will help us fetch data from an API and manage loading and error states. This is a common pattern you might use in many React apps.
Step 1: Create the Hook Function
Create a new file called useFetch.js. The hook function will accept a URL as a parameter and return data, loading status, and any error that occurs.
import { useState, useEffect } from "react"; function useFetch(url) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { if (!url) return; setLoading(true); setError(null); fetch(url) .then((response) => { if (!response.ok) { throw new Error("Network response was not ok"); } return response.json(); }) .then((json) => { setData(json); setLoading(false); }) .catch((err) => { setError(err.message); setLoading(false); }); }, [url]); return { data, loading, error }; } export default useFetch;
Step 2: Using the Hook in a Component
Now we can use this hook inside any functional component to fetch data easily.
import React from "react"; import useFetch from "./useFetch"; function UsersList() { const { data: users, loading, error } = useFetch( "https://jsonplaceholder.typicode.com/users" ); if (loading) return <p>Loading users...</p>; if (error) return <p>Error: {error}</p>; return ( <ul> {users.map((user) => ( <li key={user.id}>{user.name}</li> ))} </ul> ); } export default UsersList;
How It Works
- When the component mounts or the URL changes, the hook triggers the useEffect callback.
- It starts fetching the data and updates the loading state.
- Once data is fetched successfully, it updates data and stops loading.
- If an error occurs, it captures the error message and stops loading.
- The component renders UI based on the loading, error, and data values.
Customizing and Extending
You can extend this hook with features like:
- Adding support for request options (method, headers, body)
- Allowing manual refetching
- Supporting caching of responses
- Handling aborting requests on component unmount
These improvements will make your hook more versatile depending on your app's needs.
Conclusion
Building custom hooks in React is a powerful way to reuse and share logic across components. The useFetch example we built here is just one way to encapsulate asynchronous data fetching.
Next time you spot repetitive logic in your components, try extracting it into a custom hook. This approach helps keep your codebase clean and easier to maintain.
Would you like me to help you add code examples for advanced features or tests for this hook? Let me know!