ReactJS 5 min read

Building a Reusable useFetch Hook in React

Create a custom React hook for data fetching with loading states. Learn how to build reusable hooks that simplify API calls in functional components.

MR

Moshiour Rahman

Advertisement

Overview

Functional components with hooks have become the standard in React development. They provide an elegant way to handle state and side effects with less code than class components.

In this tutorial, we’ll create a reusable useFetch hook that handles data fetching, loading states, and can be used across your entire application.

Project Setup

Create a new React project:

npx create-react-app react-usefetch-demo
cd react-usefetch-demo

Building the useFetch Hook

Create a file src/hooks/useFetch.js:

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

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

  const fetchData = useCallback(async () => {
    try {
      setLoading(true);
      setError(null);

      const response = await fetch(url);

      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }

      const json = await response.json();
      setData(json);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  }, [url]);

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

  // Return a refetch function for manual refresh
  return { data, loading, error, refetch: fetchData };
}

export default useFetch;

What This Hook Does

  1. Manages three states: data, loading, and error
  2. Fetches automatically when the URL changes
  3. Handles errors gracefully
  4. Provides a refetch function for manual data refresh

Using the Hook

Create src/components/PhotoGallery.jsx:

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

function PhotoGallery() {
  const { data: photos, loading, error } = useFetch(
    'https://jsonplaceholder.typicode.com/photos?albumId=1'
  );

  if (loading) {
    return (
      <div className="loading">
        <div className="spinner"></div>
        <p>Loading photos...</p>
      </div>
    );
  }

  if (error) {
    return (
      <div className="error">
        <p>Error: {error}</p>
      </div>
    );
  }

  return (
    <div className="gallery">
      <h1>Photo Gallery</h1>
      <div className="photo-grid">
        {photos.map(({ id, title, thumbnailUrl }) => (
          <div key={id} className="photo-card">
            <img src={thumbnailUrl} alt={title} />
            <p>{title}</p>
          </div>
        ))}
      </div>
    </div>
  );
}

export default PhotoGallery;

Example with Refetch: User List

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

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

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

  return (
    <div>
      <div className="header">
        <h1>Users</h1>
        <button onClick={refetch}>Refresh</button>
      </div>

      <ul>
        {users.map(user => (
          <li key={user.id}>
            <strong>{user.name}</strong>
            <span>{user.email}</span>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default UserList;

Enhanced Version with Options

For more flexibility, here’s an enhanced version with configurable options:

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

function useFetch(url, options = {}) {
  const {
    immediate = true,      // Fetch immediately on mount
    initialData = null,    // Initial data state
  } = options;

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

  // Use ref to track if component is mounted
  const isMounted = useRef(true);

  const fetchData = useCallback(async (fetchOptions = {}) => {
    try {
      setLoading(true);
      setError(null);

      const response = await fetch(url, fetchOptions);

      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }

      const json = await response.json();

      // Only update state if component is still mounted
      if (isMounted.current) {
        setData(json);
      }
    } catch (err) {
      if (isMounted.current) {
        setError(err.message);
      }
    } finally {
      if (isMounted.current) {
        setLoading(false);
      }
    }
  }, [url]);

  useEffect(() => {
    isMounted.current = true;

    if (immediate) {
      fetchData();
    }

    return () => {
      isMounted.current = false;
    };
  }, [fetchData, immediate]);

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

export default useFetch;

Usage with Options

// Fetch immediately (default)
const { data } = useFetch('/api/users');

// Don't fetch immediately - useful for form submissions
const { data, refetch } = useFetch('/api/search', { immediate: false });

// With initial data - prevents flash of empty state
const { data } = useFetch('/api/posts', {
  initialData: [],
});

POST/PUT Requests

Extend the hook for mutations:

import { useState, useCallback } from 'react';

function useMutation(url) {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const mutate = useCallback(async (data, method = 'POST') => {
    try {
      setLoading(true);
      setError(null);

      const response = await fetch(url, {
        method,
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(data),
      });

      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }

      return await response.json();
    } catch (err) {
      setError(err.message);
      throw err;
    } finally {
      setLoading(false);
    }
  }, [url]);

  return { mutate, loading, error };
}

export { useMutation };

Usage

function CreateUser() {
  const { mutate, loading, error } = useMutation('/api/users');

  const handleSubmit = async (formData) => {
    try {
      const newUser = await mutate(formData);
      console.log('Created:', newUser);
    } catch (err) {
      console.error('Failed to create user');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* form fields */}
      <button type="submit" disabled={loading}>
        {loading ? 'Creating...' : 'Create User'}
      </button>
      {error && <p className="error">{error}</p>}
    </form>
  );
}

Best Practices

  1. Handle loading and error states - Always show appropriate UI
  2. Clean up on unmount - Prevent memory leaks
  3. Use useCallback - Prevent unnecessary re-renders
  4. Consider caching - For production, use libraries like React Query or SWR
  5. Type your hooks - Use TypeScript for better developer experience

Conclusion

Custom hooks are one of the most powerful features in React. The useFetch hook we built:

  • Encapsulates all fetching logic
  • Handles loading and error states
  • Is reusable across components
  • Can be easily extended

For production applications, consider using established libraries like:

  • React Query - Powerful data synchronization
  • SWR - Stale-while-revalidate strategy
  • RTK Query - If using Redux Toolkit

These provide caching, refetching, and many other features out of the box.

Key Takeaways:

  • Custom hooks promote code reuse
  • Always handle loading and error states
  • Clean up effects to prevent memory leaks
  • Consider using battle-tested libraries for production

Advertisement

MR

Moshiour Rahman

Software Architect & AI Engineer

Share:
MR

Moshiour Rahman

Software Architect & AI Engineer

Enterprise software architect with deep expertise in financial systems, distributed architecture, and AI-powered applications. Building large-scale systems at Fortune 500 companies. Specializing in LLM orchestration, multi-agent systems, and cloud-native solutions. I share battle-tested patterns from real enterprise projects.

Related Articles

Comments

Comments are powered by GitHub Discussions.

Configure Giscus at giscus.app to enable comments.