fetching with hooks

10/20/20243 min read

fetching with hooks

custom hooks let you extract logic into reusable functions. starts with "use", calls other hooks. keeps components clean.

basic useFetch

simple data fetching hook:

import { useState, useEffect } from "react";

function 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("not ok");
        const result = await response.json();
        setData(result);
      } catch (error) {
        setError(error);
      } finally {
        setLoading(false);
      }
    };
    fetchData();
  }, [url]);

  return { data, loading, error };
}

export default useFetch;

enhanced version

add refetch and options:

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

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

  const fetchData = useCallback(async () => {
    try {
      setLoading(true);
      const response = await fetch(url, options);
      if (!response.ok) throw new Error(`status: ${response.status}`);
      const result = await response.json();
      setData(result);
    } catch (e) {
      setError(e.message || 'error occurred');
    } finally {
      setLoading(false);
    }
  }, [url, options]);

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

  const refetch = useCallback(() => {
    fetchData();
  }, [fetchData]);

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

export default useFetch;

product list example

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

function ProductList() {
  const { data: products, loading, error, refetch } = useFetch('https://api.example.com/products');

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

  return (
    <div>
      <button onClick={refetch}>refresh</button>
      <ul>
        {products?.map(product => (
          <li key={product.id}>
            {product.name} - ${product.price}
          </li>
        ))}
      </ul>
    </div>
  );
}

pagination example

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

function SocialFeed() {
  const [page, setPage] = useState(1);
  const { data: posts, loading, error } = useFetch(`https://api.example.com/posts?page=${page}`);

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

  return (
    <div>
      {posts?.map(post => (
        <div key={post.id}>
          <h3>{post.title}</h3>
          <p>{post.content}</p>
        </div>
      ))}
      <button onClick={() => setPage(p => p + 1)}>load more</button>
    </div>
  );
}

debouncing

avoid unnecessary calls:

import { useState, useEffect } from 'react';

function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => clearTimeout(handler);
  }, [value, delay]);

  return debouncedValue;
}

function useDebouncedFetch(url, delay = 300) {
  const debouncedUrl = useDebounce(url, delay);
  return useFetch(debouncedUrl);
}

simple caching

const cache = new Map();

function useCachedFetch(url, options = {}) {
  const { data, loading, error, refetch } = useFetch(url, options);

  useEffect(() => {
    if (data) cache.set(url, data);
  }, [url, data]);

  return {
    data: loading ? cache.get(url) : data,
    loading,
    error,
    refetch,
  };
}

retry mechanism

exponential backoff:

function useRetryFetch(url, options = {}, maxRetries = 3) {
  const [retries, setRetries] = useState(0);
  const { data, loading, error, refetch } = useFetch(url, options);

  useEffect(() => {
    if (error && retries < maxRetries) {
      const timer = setTimeout(() => {
        setRetries(prev => prev + 1);
        refetch();
      }, 1000 * Math.pow(2, retries));

      return () => clearTimeout(timer);
    }
  }, [error, retries, maxRetries, refetch]);

  return { data, loading, error, retries };
}

tips

  • keep hooks focused on one thing
  • use typescript for better dx
  • handle race conditions
  • avoid nested hooks
  • follow rules of hooks