useFetch Hook in React
Fetching data is a common requirement in React applications. If you frequently make API calls, managing state in multiple components can become repetitive. In this post, we'll first build a component that fetches data without a custom hook and then refactor it to use a reusable useFetch hook. Fetching Data Without a Custom Hook Let's start by creating a React component that fetches data using fetch inside useEffect. import React, { useEffect, useState } from "react"; const UsersList = () => { const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const fetchUsers = async () => { try { const response = await fetch("https://jsonplaceholder.typicode.com/users"); if (!response.ok) { throw new Error("Failed to fetch users"); } const data = await response.json(); setUsers(data); } catch (err) { setError(err.message); } finally { setLoading(false); } }; fetchUsers(); }, []); if (loading) return Loading...; if (error) return Error: {error}; return ( {users.map((user) => ( {user.name} ))} ); }; export default UsersList; Issues with this approach: The API logic is tightly coupled to the component. It cannot be reused in other components. Every time you need to fetch data elsewhere, you'll need to rewrite the same logic. Creating a Custom Hook Approach 1: Using useState Let's refactor the fetching logic into a reusable useFetch hook. import { useState, useEffect } from "react"; const useFetch = (url) => { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const fetchData = async () => { try { const response = await fetch(url); if (!response.ok) { throw new Error("Failed to fetch data"); } const result = await response.json(); setData(result); } catch (err) { setError(err.message); } finally { setLoading(false); } }; fetchData(); }, [url]); return { data, loading, error }; }; export default useFetch; Advantages It centralizes API fetching logic. It can be reused across multiple components. It simplifies components by removing API call handling. Approach 2: Using useReducer Another way to structure the useFetch hook is by using useReducer instead of useState. This approach makes state management more structured. import { useReducer, useEffect } from "react"; const initialState = { data: null, loading: true, error: null, }; const fetchReducer = (state, action) => { switch (action.type) { case "FETCH_SUCCESS": return { data: action.payload, loading: false, error: null }; case "FETCH_ERROR": return { data: null, loading: false, error: action.payload }; default: return state; } }; const useFetch = (url) => { const [state, dispatch] = useReducer(fetchReducer, initialState); useEffect(() => { const fetchData = async () => { try { const response = await fetch(url); if (!response.ok) { throw new Error("Failed to fetch data"); } const result = await response.json(); dispatch({ type: "FETCH_SUCCESS", payload: result }); } catch (err) { dispatch({ type: "FETCH_ERROR", payload: err.message }); } }; fetchData(); }, [url]); return state; }; export default useFetch; Using the Custom Hook in a Component Now, our UsersList component is much cleaner: import React from "react"; import useFetch from "./useFetch"; const UsersList = () => { const { data: users, loading, error } = useFetch("https://jsonplaceholder.typicode.com/users"); if (loading) return Loading...; if (error) return Error: {error}; return ( {users.map((user) => ( {user.name} ))} ); }; export default UsersList; By using useFetch, we have separated the data fetching logic from our component, making it more modular and reusable.

Fetching data is a common requirement in React applications. If you frequently make API calls, managing state in multiple components can become repetitive.
In this post, we'll first build a component that fetches data without a custom hook and then refactor it to use a reusable useFetch
hook.
Fetching Data Without a Custom Hook
Let's start by creating a React component that fetches data using fetch
inside useEffect
.
import React, { useEffect, useState } from "react";
const UsersList = () => {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchUsers = async () => {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/users");
if (!response.ok) {
throw new Error("Failed to fetch users");
}
const data = await response.json();
setUsers(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchUsers();
}, []);
if (loading) return <p>Loading...p>;
if (error) return <p>Error: {error}p>;
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}li>
))}
ul>
);
};
export default UsersList;
Issues with this approach:
- The API logic is tightly coupled to the component.
- It cannot be reused in other components.
- Every time you need to fetch data elsewhere, you'll need to rewrite the same logic.
Creating a Custom Hook
Approach 1: Using useState
Let's refactor the fetching logic into a reusable useFetch
hook.
import { useState, useEffect } from "react";
const useFetch = (url) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error("Failed to fetch data");
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
};
export default useFetch;
Advantages
- It centralizes API fetching logic.
- It can be reused across multiple components.
- It simplifies components by removing API call handling.
Approach 2: Using useReducer
Another way to structure the useFetch
hook is by using useReducer
instead of useState
. This approach makes state management more structured.
import { useReducer, useEffect } from "react";
const initialState = {
data: null,
loading: true,
error: null,
};
const fetchReducer = (state, action) => {
switch (action.type) {
case "FETCH_SUCCESS":
return { data: action.payload, loading: false, error: null };
case "FETCH_ERROR":
return { data: null, loading: false, error: action.payload };
default:
return state;
}
};
const useFetch = (url) => {
const [state, dispatch] = useReducer(fetchReducer, initialState);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error("Failed to fetch data");
}
const result = await response.json();
dispatch({ type: "FETCH_SUCCESS", payload: result });
} catch (err) {
dispatch({ type: "FETCH_ERROR", payload: err.message });
}
};
fetchData();
}, [url]);
return state;
};
export default useFetch;
Using the Custom Hook in a Component
Now, our UsersList
component is much cleaner:
import React from "react";
import useFetch from "./useFetch";
const UsersList = () => {
const { data: users, loading, error } = useFetch("https://jsonplaceholder.typicode.com/users");
if (loading) return <p>Loading...p>;
if (error) return <p>Error: {error}p>;
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}li>
))}
ul>
);
};
export default UsersList;
By using useFetch
, we have separated the data fetching logic from our component, making it more modular and reusable.