fetching with hooks
10/20/2024•3 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