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.
Top comments (0)