React  

How to Create Custom Hooks in React for API Calls

Introduction

Custom hooks in React enable you to extract reusable logic — such as making API requests — into neat, reusable functions. This makes your components cleaner, easier to read, and simpler to test. In this guide, you'll learn how to create safe, efficient, and reusable custom hooks for fetching data from APIs in React.

Why Use Custom Hooks for API Calls?

  1. Reusability — Put common API logic (fetching, loading state, error handling) in one place and reuse it across components.

  2. Cleaner Components — Components become mostly about UI; data fetching moves into hooks.

  3. Testability — Hooks are easy to test independently from UI.

  4. Consistency — One hook enforces a consistent pattern for all API calls (caching, retries, aborting).

Using custom hooks helps teams, especially developers in India, the USA, and the UK, keep large React apps maintainable and fast.

Basic Pattern: useFetch Hook

A simple reusable hook for GET requests. It manages three states: data, loading, and error.

import { useState, useEffect } from 'react';

export function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    if (!url) return;

    let isMounted = true; // avoid state update after unmount

    async function fetchData() {
      setLoading(true);
      try {
        const res = await fetch(url);
        if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
        const json = await res.json();
        if (isMounted) setData(json);
      } catch (err) {
        if (isMounted) setError(err);
      } finally {
        if (isMounted) setLoading(false);
      }
    }

    fetchData();

    return () => {
      isMounted = false;
    };
  }, [url]);

  return { data, loading, error };
}

How to use it in a component:

import React from 'react';
import { useFetch } from './hooks/useFetch';

function Posts() {
  const { data, loading, error } = useFetch('https://jsonplaceholder.typicode.com/posts');

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <ul>
      {data.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

export default Posts;

Advanced Hook: useApi with POST, cancelation, and headers

For real apps you often need more control: custom headers, POST requests, request cancellation (AbortController), and manual triggers.

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

export function useApi(defaultUrl = '', defaultOptions = {}) {
  const [url, setUrl] = useState(defaultUrl);
  const [options, setOptions] = useState(defaultOptions);
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const abortCtrlRef = useRef(null);

  const callApi = useCallback(async (callUrl = url, callOptions = options) => {
    if (!callUrl) return;
    setLoading(true);
    setError(null);
    abortCtrlRef.current?.abort(); // abort previous
    const controller = new AbortController();
    abortCtrlRef.current = controller;

    try {
      const res = await fetch(callUrl, { signal: controller.signal, ...callOptions });
      if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
      const json = await res.json();
      setData(json);
      return json;
    } catch (err) {
      if (err.name === 'AbortError') {
        // request was cancelled
        return;
      }
      setError(err);
      throw err;
    } finally {
      setLoading(false);
    }
  }, [url, options]);

  useEffect(() => {
    // optional: auto-call when url changes
    if (url) callApi();
    return () => abortCtrlRef.current?.abort();
  }, [url, callApi]);

  return {
    data,
    loading,
    error,
    callApi,
    setUrl,
    setOptions,
  };
}

Example usage for POST:

function CreatePost() {
  const { data, loading, error, callApi } = useApi();

  const handleSubmit = async (formData) => {
    try {
      const json = await callApi('https://jsonplaceholder.typicode.com/posts', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(formData),
      });
      console.log('Created', json);
    } catch (err) {
      console.error(err);
    }
  };

  return (
    <div>
      {/* form code */}
      {loading && <p>Creating post...</p>}
      {error && <p>Error: {error.message}</p>}
    </div>
  );
}

Using useSWR or React Query Instead of Building From Scratch

For caching, background revalidation, retries, pagination, and more advanced features, consider libraries:

  • SWR by Vercel (swr package)

  • React Query (now TanStack Query)

These libraries handle caching and revalidation for you with small APIs like useSWR(url, fetcher) or useQuery from React Query. They are great for production apps and large teams.

Example with SWR:

import useSWR from 'swr';

const fetcher = (url) => fetch(url).then(res => res.json());

function UserList() {
  const { data, error } = useSWR('https://jsonplaceholder.typicode.com/users', fetcher);

  if (!data) return <div>Loading...</div>;
  if (error) return <div>Error</div>;

  return (
    <ul>
      {data.map(u => <li key={u.id}>{u.name}</li>)}
    </ul>
  );
}

Tips for Robust API Hooks

  1. Handle Abort / Cancelation: Prevent state updates after unmount and cancel slow requests using AbortController.

  2. Error Handling: Provide helpful error messages and consider retry logic for transient failures.

  3. Caching: Cache results where useful to reduce network calls.

  4. Debounce Inputs: When calling APIs from user input, debounce to avoid too many requests.

  5. Authentication: Centralize auth headers (tokens) and refresh flows in the hook or a wrapper.

  6. Environment Variables: Use process.env for base URLs to handle different environments (development, production).

  7. Testing: Mock fetch in tests with tools like msw (Mock Service Worker) to test hooks without real network calls.

Accessibility and SEO Notes

  • For API-driven content that affects SEO (server-rendered lists, product pages), prefer server-side rendering (SSR) or static generation (SSG) in frameworks like Next.js. Client-side hooks are great for user dashboards and interactive UIs but may not be crawled by search engines as easily.

  • Ensure loading states and error messages are accessible (ARIA) and provide useful content for screen readers.

Summary

Custom hooks in React simplify and standardize how your app interacts with APIs. Start with a simple useFetch for GET requests, then move to a more flexible useApi for POSTs, headers, and cancellation. For advanced needs, use battle-tested libraries like SWR or React Query. Follow best practices like aborting requests, handling errors, caching, and keeping environment-specific URLs in process.env for cleaner deployments across India, USA, UK, and other regions.