Trying to fetch data inside render() causing multiple requests

A common performance issue in React occurs when data fetching is improperly placed inside render methods or function components, causing multiple unnecessary network requests. Here’s how to properly structure data fetching to avoid this problem.

The Core Problem

Incorrect implementations that trigger multiple requests:

// ❌ Class component - fetch in render()
class UserProfile extends React.Component {
  render() {
    fetch('/api/user') // Bad - runs every render
      .then(res => res.json())
      .then(data => this.setState({ user: data }));

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

// ❌ Function component - direct fetch
function UserProfile() {
  const [user, setUser] = useState(null);

  fetch('/api/user') // Bad - runs every render
    .then(res => res.json())
    .then(setUser);

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

Correct Implementation

1. Class Component – Use componentDidMount

class UserProfile extends React.Component {
  state = { user: null };

  componentDidMount() {
    fetch('/api/user') // ✅ Runs once on mount
      .then(res => res.json())
      .then(user => this.setState({ user }));
  }

  render() {
    return <div>{this.state.user?.name}</div>;
  }
}

2. Function Component – Use useEffect

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

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

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

Common Mistakes and Solutions

❌ Missing Dependency Array

useEffect(() => {
  fetchData(); // ❌ Runs after every render
}); // Missing dependency array

Fix: Add empty dependency array for mount-only

useEffect(() => {
  fetchData(); // ✅ Runs once on mount
}, []); // Empty array

❌ Incorrect Dependencies

useEffect(() => {
  fetchUser(userId); // ❌ Missing userId dependency
}, []); // Will use stale userId

Fix: Include all dependencies

useEffect(() => {
  fetchUser(userId); // ✅ Re-runs when userId changes
}, [userId]); // Proper dependencies

❌ Async Functions Without Cleanup

useEffect(() => {
  const fetchData = async () => {
    const data = await fetchUser();
    setUser(data); // ❌ Potential state update after unmount
  };
  fetchData();
}, []);

Fix: Add cleanup/cancellation

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

  const fetchData = async () => {
    const data = await fetchUser();
    if (isMounted) setUser(data); // ✅ Safe update
  };
  fetchData();

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

Advanced Patterns

1. Custom Hook for Data Fetching

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

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

    const fetchData = async () => {
      try {
        const response = await fetch(url, options);
        if (!response.ok) throw new Error(response.statusText);
        const json = await response.json();
        if (isMounted) {
          setData(json);
          setError(null);
        }
      } catch (err) {
        if (isMounted) {
          setError(err.message);
          setData(null);
        }
      } finally {
        if (isMounted) setLoading(false);
      }
    };

    fetchData();

    return () => {
      isMounted = false;
    };
  }, [url, options]);

  return { data, error, loading };
}

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

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

2. Cancellation with AbortController

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

  const fetchData = async () => {
    try {
      const response = await fetch('/api/user', {
        signal: controller.signal
      });
      const user = await response.json();
      setUser(user);
    } catch (err) {
      if (err.name !== 'AbortError') {
        setError(err.message);
      }
    }
  };

  fetchData();

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

3. Debouncing Frequent Changes

useEffect(() => {
  const timerId = setTimeout(() => {
    fetchResults(searchQuery);
  }, 300); // Wait 300ms after last change

  return () => clearTimeout(timerId); // Cleanup previous timer
}, [searchQuery]); // Runs when searchQuery changes

Best Practices

  1. Fetch in useEffect (function components) or componentDidMount (class components)
  2. Include proper dependencies in useEffect
  3. Implement cleanup to prevent memory leaks
  4. Handle loading/error states for better UX
  5. Consider custom hooks for reusable fetch logic
  6. Cancel pending requests on unmount
  7. Debounce rapid changes for search/input fields

Key Takeaways

  1. Never place fetches in render – they’ll run too often
  2. Use lifecycle methods appropriately for data fetching
  3. Handle component unmounting to prevent memory leaks
  4. Consider edge cases like slow networks and errors
  5. Optimize performance with debouncing and caching

Proper data fetching architecture prevents performance issues and ensures your React components behave predictably when loading asynchronous data.

Leave a Reply

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