Skip to content

Latest commit

 

History

History
311 lines (240 loc) · 8.25 KB

File metadata and controls

311 lines (240 loc) · 8.25 KB

Data Fetching in React

Fetching Strategy Decision Tree

flowchart TD
    A[Need to fetch data?] --> B{Who needs this data?}
    B -- One component --> C{Fetch complexity?}
    B -- Many components --> D{Framework / server?}

    C -- Simple --> E[useEffect + useState]
    C -- Complex / reused --> F[useFetch custom hook]

    D -- Next.js App Router --> G[Server Component\nasync/await direct]
    D -- Client-only --> H[React Query / SWR]

    H --> I[Caching, dedup, background refetch]
    G --> J[Zero client JS, streaming]
Loading

Pattern 1 — Basic useEffect fetch

Good for simple, one-off fetches in a single component.

import { useState, useEffect } from 'react';

function PostList() {
  const [posts,   setPosts]   = useState([]);
  const [loading, setLoading] = useState(true);
  const [error,   setError]   = useState(null);

  useEffect(() => {
    let cancelled = false;

    fetch('https://jsonplaceholder.typicode.com/posts?_limit=5')
      .then(res => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res.json();
      })
      .then(data => {
        if (!cancelled) setPosts(data);
      })
      .catch(err => {
        if (!cancelled) setError(err.message);
      })
      .finally(() => {
        if (!cancelled) setLoading(false);
      });

    return () => { cancelled = true; }; // cleanup — prevent setState on unmounted component
  }, []); // empty deps = fetch once on mount

  if (loading) return <p>Loading…</p>;
  if (error)   return <p>Error: {error}</p>;
  return (
    <ul>
      {posts.map(post => <li key={post.id}>{post.title}</li>)}
    </ul>
  );
}

Pattern 2 — Fetch on dependency change

Re-fetch whenever a prop or state value changes.

import { useState, useEffect } from 'react';

function UserDetail({ userId }) {
  const [user,    setUser]    = useState(null);
  const [loading, setLoading] = useState(false);
  const [error,   setError]   = useState(null);

  useEffect(() => {
    if (!userId) return;

    const controller = new AbortController();
    setLoading(true);
    setUser(null);
    setError(null);

    fetch(`/api/users/${userId}`, { signal: controller.signal })
      .then(res => res.json())
      .then(setUser)
      .catch(err => {
        if (err.name !== 'AbortError') setError(err.message);
      })
      .finally(() => setLoading(false));

    return () => controller.abort(); // cancel if userId changes before fetch resolves
  }, [userId]); // re-fetch when userId changes

  if (!userId)  return <p>Select a user</p>;
  if (loading)  return <p>Loading user {userId}</p>;
  if (error)    return <p>Error: {error}</p>;
  return <p>{user?.name}</p>;
}

Pattern 3 — Parallel fetches

Fetch multiple resources at the same time using Promise.all.

import { useState, useEffect } from 'react';

function Dashboard() {
  const [data,    setData]    = useState(null);
  const [loading, setLoading] = useState(true);
  const [error,   setError]   = useState(null);

  useEffect(() => {
    Promise.all([
      fetch('/api/stats').then(r => r.json()),
      fetch('/api/activity').then(r => r.json()),
      fetch('/api/notifications').then(r => r.json()),
    ])
      .then(([stats, activity, notifications]) => {
        setData({ stats, activity, notifications });
      })
      .catch(err => setError(err.message))
      .finally(() => setLoading(false));
  }, []);

  if (loading) return <Skeleton />;
  if (error)   return <ErrorBanner message={error} />;

  return (
    <>
      <StatsPanel data={data.stats} />
      <ActivityFeed items={data.activity} />
      <NotificationBell items={data.notifications} />
    </>
  );
}

Pattern 4 — Async/await with async IIFE

Cleaner async syntax inside useEffect.

useEffect(() => {
  // useEffect callback can't be async directly
  // Use an IIFE to enable async/await
  (async () => {
    try {
      const res  = await fetch('/api/data');
      const json = await res.json();
      setData(json);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  })();
}, []);

Pattern 5 — React Query (recommended for production)

Handles caching, background refetch, deduplication, and error retries.

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

// GET
function TodoList() {
  const { data, isLoading, isError } = useQuery({
    queryKey: ['todos'],
    queryFn: () => fetch('/api/todos').then(r => r.json()),
    staleTime: 60_000,  // cache fresh for 1 minute
  });

  if (isLoading) return <Spinner />;
  if (isError)   return <p>Failed to load</p>;
  return <ul>{data.map(t => <li key={t.id}>{t.title}</li>)}</ul>;
}

// POST / mutation
function AddTodo() {
  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: (text) => fetch('/api/todos', {
      method: 'POST',
      body: JSON.stringify({ text }),
      headers: { 'Content-Type': 'application/json' },
    }).then(r => r.json()),

    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['todos'] }); // refetch list
    },
  });

  return (
    <button onClick={() => mutation.mutate('New task')} disabled={mutation.isPending}>
      {mutation.isPending ? 'Adding…' : 'Add Todo'}
    </button>
  );
}

Pattern 6 — Optimistic updates

Update the UI immediately, then sync with the server. Roll back on failure.

import { useQueryClient, useMutation } from '@tanstack/react-query';

function LikeButton({ postId, initialLikes }) {
  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: () => fetch(`/api/posts/${postId}/like`, { method: 'POST' }).then(r => r.json()),

    onMutate: async () => {
      await queryClient.cancelQueries({ queryKey: ['post', postId] });
      const prev = queryClient.getQueryData(['post', postId]);

      // Optimistically update cache
      queryClient.setQueryData(['post', postId], old => ({
        ...old,
        likes: old.likes + 1,
      }));

      return { prev }; // context for rollback
    },

    onError: (err, _, context) => {
      queryClient.setQueryData(['post', postId], context.prev); // rollback
    },

    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ['post', postId] }); // sync
    },
  });

  return (
    <button onClick={() => mutation.mutate()}>
      ❤️ {initialLikes}
    </button>
  );
}

Request lifecycle diagram

sequenceDiagram
    participant C as Component
    participant H as useFetch / useQuery
    participant N as Network
    participant S as Server

    C->>H: Mount / dep changes
    H->>H: Set loading=true
    H->>N: fetch(url, { signal })
    N->>S: HTTP GET
    S-->>N: 200 OK + JSON
    N-->>H: Response
    H->>H: setData(json), loading=false
    H-->>C: Re-render with data

    Note over C,H: User navigates away
    C->>H: Cleanup (unmount)
    H->>N: controller.abort()
    N--xS: Request cancelled
Loading

Common mistakes

Mistake Problem Fix
async directly in useEffect Returns a Promise, not a cleanup function Use IIFE or inner async function
No cleanup / no abort Memory leaks, stale setState on unmounted component AbortController + cleanup
Missing loading state Flash of empty UI Always initialise loading: true
Fetching in render New request on every render Always inside useEffect
[] deps with used variables Stale closure Add all used values to deps array
Catching AbortError Shows error on intentional cancellation Check err.name !== 'AbortError'

Interview questions on this topic

  1. Why can't useEffect be async?

    useEffect must return either undefined or a cleanup function. An async function always returns a Promise, which React silently ignores — breaking cleanup.

  2. What is the race condition problem in data fetching?

    If userId changes quickly, two requests may be in-flight simultaneously. The slower one could resolve last and overwrite the correct data. Fix: cancel the previous request with AbortController or a cancelled boolean flag.

  3. What does staleTime do in React Query?

    It defines how long cached data is considered fresh. During that window, React Query serves the cache without making a network request.