DEV Community

Joodi
Joodi

Posted on

useFetch Hook in React

Image description
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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

By using useFetch, we have separated the data fetching logic from our component, making it more modular and reusable.

Top comments (1)

Collapse
 
santoshkp27 profile image
Santosh Kumar

Thank you! Great explanation