Trying to fetch data inside render() causing multiple requests

Loading

Preventing Multiple API Requests in React Components

A common performance issue in React applications occurs when data fetching is incorrectly placed in the render method or directly in the component body, causing multiple unnecessary requests.

The Problem: Uncontrolled Data Fetching

// ❌ Wrong approach - causes infinite re-renders
function UserProfile() {
  const [user, setUser] = useState(null);

  // Executes on every render
  fetch('/api/user')
    .then(res => res.json())
    .then(data => setUser(data));

  return <div>{user?.name}</div>;
}

Why This Happens

  1. Render Cycle: Fetch triggers state update → re-render → fetch again
  2. No Dependency Control: Runs on every component update
  3. Memory Leaks: Potential for unresolved promises after unmount
  4. Performance Impact: Multiple identical API calls

Correct Implementation Patterns

1. Using useEffect (Basic)

function UserProfile() {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch('/api/user')
      .then(res => res.json())
      .then(data => setUser(data));
  }, []); // Empty dependency array = runs once on mount

  return <div>{user?.name}</div>;
}

2. With Loading and Error States

function UserProfile() {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let isMounted = true;

    const fetchData = async () => {
      try {
        const response = await fetch('/api/user');
        if (!response.ok) throw new Error('Request failed');
        const data = await response.json();
        if (isMounted) setUser(data);
      } catch (err) {
        if (isMounted) setError(err.message);
      } finally {
        if (isMounted) setLoading(false);
      }
    };

    fetchData();

    return () => {
      isMounted = false; // Cleanup function
    };
  }, []);

  if (loading) return <Loader />;
  if (error) return <Error message={error} />;
  return <div>{user.name}</div>;
}

3. With Dependency-Based Fetching

function UserPosts({ userId }) {
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    fetch(`/api/users/${userId}/posts`)
      .then(res => res.json())
      .then(data => setPosts(data));
  }, [userId]); // Re-fetches when userId changes

  return <PostList posts={posts} />;
}

4. Using AbortController

useEffect(() => {
  const controller = new AbortController();

  const fetchData = async () => {
    try {
      const response = await fetch('/api/data', {
        signal: controller.signal
      });
      const data = await response.json();
      setData(data);
    } catch (error) {
      if (error.name !== 'AbortError') {
        console.error('Fetch error:', error);
      }
    }
  };

  fetchData();

  return () => {
    controller.abort(); // Cancel request on unmount
  };
}, []);

Best Practices

  1. Always Use useEffect: For side effects like data fetching
  2. Include Dependencies: List all dependencies in the array
  3. Clean Up Resources: Cancel requests on unmount
  4. Error Handling: Always catch potential errors
  5. Loading States: Provide visual feedback during requests

Common Pitfalls

  1. Missing Dependency Array:
   useEffect(() => {
     // Runs after every render
   }); // ❌ Missing array
  1. Incorrect Dependencies:
   useEffect(() => {
     fetchData(userId);
   }, []); // ❌ Missing userId dependency
  1. Stale Closures:
   useEffect(() => {
     fetch(`/api/user/${userId}`)
       .then(res => res.json())
       .then(data => setUser(data));
   }, []); // ❌ Uses stale userId if it changes
  1. Race Conditions:
   useEffect(() => {
     fetch(`/api/users/${userId}`)
       .then(res => res.json())
       .then(data => setUser(data));
   }, [userId]); // ❌ Earlier requests may resolve after later ones

Advanced Solutions

1. Custom Fetch Hook

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

  useEffect(() => {
    const controller = new AbortController();

    const fetchData = async () => {
      try {
        const response = await fetch(url, {
          ...options,
          signal: controller.signal
        });
        const json = await response.json();
        setData(json);
      } catch (err) {
        if (err.name !== 'AbortError') {
          setError(err);
        }
      } finally {
        setLoading(false);
      }
    };

    fetchData();

    return () => controller.abort();
  }, [url]);

  return { data, error, loading };
}

// Usage
function UserProfile() {
  const { data: user, loading, error } = useFetch('/api/user');
  // ...
}

2. Using React Query (Recommended)

import { useQuery } from 'react-query';

function UserProfile() {
  const { data: user, isLoading, error } = useQuery('user', () =>
    fetch('/api/user').then(res => res.json())
  );

  if (isLoading) return <Loader />;
  if (error) return <Error message={error.message} />;

  return <div>{user.name}</div>;
}

3. Debouncing Rapid Changes

function SearchResults({ query }) {
  const [results, setResults] = useState([]);

  useEffect(() => {
    const timer = setTimeout(() => {
      if (query) {
        fetch(`/api/search?q=${query}`)
          .then(res => res.json())
          .then(data => setResults(data));
      }
    }, 300); // Wait 300ms after last change

    return () => clearTimeout(timer);
  }, [query]);

  return <ResultsList items={results} />;
}

Remember: Data fetching should always be treated as a side effect in React components. By properly structuring your API calls within useEffect with appropriate dependencies and cleanup, you ensure efficient, controlled data fetching without unnecessary requests.

Leave a Reply

Your email address will not be published. Required fields are marked *