By : admin
Language: javascript
Date Published: 4 days, 3 hours ago
Custom hooks encapsulate reusable stateful logic, making components cleaner and promoting code reuse. For data fetching, a well-designed hook handles loading states, errors, caching, and request deduplication—common concerns across API calls. Instead of repeating useEffect, useState, and try/catch blocks in every component, abstract this pattern into a hook like useApi. This approach improves consistency, reduces boilerplate, and centralizes error handling and retry logic. Key considerations include stabilizing dependencies, managing race conditions, and integrating with React Query or SWR if adopted later. For financial or e-commerce apps where data reliability impacts user trust, such hooks ensure uniform behavior: showing skeletons on load, displaying helpful error messages, and preventing stale UI updates. They also facilitate testing—you can unit test the hook in isolation with mocked fetch implementations. Treat custom hooks as shared utilities; document their contracts clearly and version them if shared across projects.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | // hooks/useApi.js import { useState, useEffect, useCallback, useRef } from "react"; function useApi(url, options = {}) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [lastUrl, setLastUrl] = useState(null); // Ref to track if component is mounted (avoid state updates on unmounted) const isMounted = useRef(true); useEffect(() => { return () => { isMounted.current = false; }; }, []); const fetchData = useCallback(async () => { if (!url) return; setLoading(true); setError(null); try { const response = await fetch(url, options); if (!response.ok) throw new Error(`HTTP ${response.status}`); const result = await response.json(); // Only update state if component still mounted and URL hasn't changed if (isMounted.current && url === lastUrl) { setData(result); setLoading(false); } } catch (err) { if (isMounted.current && url === lastUrl) { setError(err.message); setLoading(false); } } }, [url, options, lastUrl]); // Reset when URL changes useEffect(() => { setLastUrl(url); fetchData(); }, [url, fetchData]); // Expose refetch function for manual triggers const refetch = useCallback(() => { fetchData(); }, [fetchData]); return { data, loading, error, refetch }; } export default useApi; // Usage example in a component // ProductCard.jsx import React from "react"; import useApi from "../hooks/useApi"; export default function ProductCard({ productId }) { const { data: product, loading, error } = useApi( `/api/products/${productId}` ); if (loading) return <div className="skeleton-loader">Loading...</div>; if (error) return <div className="error">Failed to load product</div>; return ( <div className="product-card"> <h3>{product?.name}</h3> <p>${product?.price?.toFixed(2)}</p> <button>Add to cart</button> </div> ); } // Example with custom configuration (headers, method) // useApi("/api/user/profile", { headers: { Authorization: `Bearer ${token}`} }) |