Skip to Content
FrontendReactIntermediate2.4 Client-side Data Fetching

Data Fetching (Client-side) 🌐

Fetch strategies keep UIs in sync with remote APIs. Handle lifecycle carefully to avoid leaks or inconsistent UI.

1) Fetch in useEffect 🔄

import { useEffect, useState } from "react"; function PostsList() { const [posts, setPosts] = useState([]); useEffect(() => { async function load() { const res = await fetch("/api/posts"); setPosts(await res.json()); } load(); }, []); return <Posts items={posts} />; }
  • Trigger network requests inside effects to avoid repeated fetches.
  • Store data, loading, and error state together for clarity.

2) Loading & Error States 🧠

const [state, setState] = useState({ status: "idle", data: null, error: null }); useEffect(() => { let mounted = true; setState(prev => ({ ...prev, status: "loading" })); fetch(`/api/users/${id}`) .then(res => res.json()) .then(data => mounted && setState({ status: "success", data, error: null })) .catch(error => mounted && setState({ status: "error", data: null, error })); return () => { mounted = false; }; }, [id]);
  • Represent states explicitly: idle → loading → success/error.
  • Show skeletons or spinners during loading; render fallback UI for errors.

3) AbortController & Cleanup 🧹

  • Cancel fetches when components unmount or parameters change mid-request.
useEffect(() => { const controller = new AbortController(); async function load() { try { const res = await fetch(`/api/search?q=${query}`, { signal: controller.signal }); setResults(await res.json()); } catch (error) { if (error.name !== "AbortError") { setError(error); } } } load(); return () => controller.abort(); }, [query]);

4) Pagination & Infinite Scroll 🔄

  • Store page index and append results.
  • Debounce scroll listeners to avoid floods.
const [page, setPage] = useState(1); const [items, setItems] = useState([]); useEffect(() => { let ignore = false; async function load() { setLoading(true); const res = await fetch(`/api/products?page=${page}`); const data = await res.json(); if (!ignore) setItems(prev => [...prev, ...data]); setLoading(false); } load(); return () => { ignore = true; }; }, [page]);
  • Trigger setPage(page + 1) when the sentinel element intersects the viewport (IntersectionObserver) or when reaching scroll thresholds.

5) Caching Basics 🧱

  • Cache responses in memory to reuse for identical queries.
const cache = new Map(); async function fetchWithCache(url) { if (cache.has(url)) return cache.get(url); const response = await fetch(url); const data = await response.json(); cache.set(url, data); return data; }
  • Libraries like React Query / SWR handle cache invalidation, background revalidation, and deduplication out of the box.

Key Takeaways ✅

  • Always manage status, data, and error explicitly to keep UI honest.
  • Use AbortController or flags to avoid updating unmounted components.
  • Infinite scroll and pagination need append logic plus guards against duplicate fetches.
  • Caching smooths UX; consider dedicated data-fetching libraries for complex scenarios.

Recap 🔄

Fetching data client-side means running requests in useEffect, tracking lifecycle states, cleaning up with AbortController, and planning for pagination or caching. Build deterministic flows so the UI never shows stale or conflicting information.

Last updated on