5 min read

Building a React Hook for API Calls

A practical example of creating a reusable React hook for handling API calls with loading states and error handling

One of the most common patterns in React applications is making API calls and managing the associated loading and error states. Let’s build a custom hook that makes this process cleaner and more reusable.

The Problem

Every time we make an API call, we end up writing similar boilerplate code:

const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);

The Solution: use Api Hook

Here’s a custom hook that encapsulates this pattern:

import { useState, useEffect, useCallback } from 'react';

const useApi = (url, options = {}) => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const fetchData = useCallback(async () => {
    setLoading(true);
    setError(null);
    
    try {
      const response = await fetch(url, {
        headers: {
          'Content-Type': 'application/json',
          ...options.headers,
        },
        ...options,
      });
      
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      
      const result = await response.json();
      setData(result);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  }, [url, options]);

  useEffect(() => {
    if (url) {
      fetchData();
    }
  }, [fetchData]);

  return { data, loading, error, refetch: fetchData };
};

export default useApi;

Usage Example

Now using the hook becomes much cleaner:

import useApi from './hooks/useApi';

const UserProfile = ({ userId }) => {
  const { data: user, loading, error, refetch } = useApi(
    `/api/users/${userId}`
  );

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  if (!user) return <div>No user found</div>;

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
      <button onClick={refetch}>Refresh</button>
    </div>
  );
};

Key Benefits

  • Reusable: Works with any API endpoint
  • Clean: Eliminates boilerplate code
  • Flexible: Supports custom options and headers
  • Reliable: Includes proper error handling and loading states

This pattern has saved me countless hours of repetitive code writing, and I hope it helps you too!