JAVASCRIPT

Build a Reusable `useFetch` Hook for Data Loading in React

Build a reusable `useFetch` hook for React. Encapsulate data fetching logic, including loading, error handling, and data states, for declarative and efficient API calls.

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

function useFetch(url, options) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const optionsRef = useRef(options); // To prevent infinite loop if options object is not memoized externally

  useEffect(() => {
    const abortController = new AbortController();
    const signal = abortController.signal;

    const fetchData = async () => {
      setLoading(true);
      setError(null);
      try {
        const response = await fetch(url, { ...optionsRef.current, signal });
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        const json = await response.json();
        setData(json);
      } catch (e) {
        if (e.name === 'AbortError') {
          console.log('Fetch aborted');
          return;
        }
        setError(e);
        setData(null);
      } finally {
        setLoading(false);
      }
    };

    if (url) {
      fetchData();
    }

    return () => {
      abortController.abort(); // Cleanup: abort ongoing fetch request
    };
  }, [url]); // Re-run effect if URL changes

  return { data, loading, error };
}

// Example Usage:
function DataDisplay() {
  const { data, loading, error } = useFetch('https://jsonplaceholder.typicode.com/posts/1');
  const {
    data: todoData,
    loading: todoLoading,
    error: todoError
  } = useFetch('https://jsonplaceholder.typicode.com/todos/1');

  const containerStyle = {
    padding: '20px',
    border: '1px solid #eee',
    borderRadius: '8px',
    maxWidth: '600px',
    margin: '20px auto',
    textAlign: 'left'
  };

  const sectionStyle = {
    marginBottom: '20px',
    borderBottom: '1px dashed #ccc',
    paddingBottom: '15px'
  };

  if (loading || todoLoading) return <div style={containerStyle}>Loading data...</div>;
  if (error) return <div style={containerStyle}><p style={{color: 'red'}}>Error: {error.message}</p></div>;
  if (todoError) return <div style={containerStyle}><p style={{color: 'red'}}>Error fetching todo: {todoError.message}</p></div>;

  return (
    <div style={containerStyle}>
      <h2>Data from API Calls:</h2>
      <div style={sectionStyle}>
        <h3>Post Data:</h3>
        {data ? (
          <>
            <p><strong>Title:</strong> {data.title}</p>
            <p><strong>Body:</strong> {data.body}</p>
          </>
        ) : (
          <p>No post data available.</p>
        )}
      </div>

      <div style={sectionStyle}>
        <h3>Todo Data:</h3>
        {todoData ? (
          <>
            <p><strong>Title:</strong> {todoData.title}</p>
            <p><strong>Completed:</strong> {todoData.completed ? 'Yes' : 'No'}</p>
          </>
        ) : (
          <p>No todo data available.</p>
        )}
      </div>
    </div>
  );
}

export default DataDisplay;
How it works: This custom `useFetch` hook encapsulates the logic for asynchronous data fetching, managing loading, error, and success states. It leverages `useEffect` to initiate the fetch when the component mounts or the `url` dependency changes, and includes an `AbortController` for proper cleanup to prevent memory leaks or state updates on unmounted components. `useRef` is used for `options` to ensure that if the options object isn't memoized externally, it doesn't cause an infinite loop in the `useEffect`'s dependency array.

Need help integrating this into your project?

Our team of expert developers can help you build your custom application from scratch.

Hire DigitalCodeLabs