Trying to fetch data inside render() causing multiple requests

Loading

Avoiding Multiple Data Fetches in React Components

A common React anti-pattern is placing data fetching logic directly in the render method or function component body, which can cause:

  • Infinite request loops
  • Performance issues
  • API rate limit problems
  • Inconsistent UI state

The Problem (Incorrect Fetching)

// ❌ Wrong - Fetches on every render
function UserProfile() {
  const [user, setUser] = useState(null);

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

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

Correct Solutions

1. useEffect with Empty Dependencies (Recommended)

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

  useEffect(() => {
    // ✅ Runs once on mount
    fetch('/api/user')
      .then(res => {
        if (!res.ok) throw new Error(res.statusText);
        return res.json();
      })
      .then(data => setUser(data))
      .catch(err => setError(err))
      .finally(() => setLoading(false));
  }, []); // Empty array = run once

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

2. Using AbortController for Cleanup

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

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

  fetchData();

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

3. With Dependency Tracking

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

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

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

Common Mistakes to Avoid

  1. Forgetting dependency arrays:
   useEffect(() => {
     fetchData(); // ❌ Runs on every render
   }); // Missing dependency array
  1. Incorrect dependencies:
   useEffect(() => {
     fetchData(id);
   }, []); // ❌ Missing id dependency
  1. Async functions directly in useEffect:
   useEffect(async () => { // ❌ Can't return cleanup from async
     const data = await fetchData();
     setData(data);
   }, []);
  1. Fetching in event handlers without state:
   function Search() {
     const handleSearch = (query) => {
       fetchResults(query); // ❌ Should store results in state
     };
     return <input onChange={e => handleSearch(e.target.value)} />;
   }

Best Practices

  1. Always use useEffect for data fetching
  2. Include proper dependencies – Empty array for mount-only
  3. Handle loading/error states – Don’t assume requests succeed
  4. Clean up requests with AbortController
  5. Memoize expensive operations with useMemo
  6. Consider custom hooks for reusable fetching logic:
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 res = await fetch(url, { 
          ...options, 
          signal: controller.signal 
        });
        const json = await res.json();
        setData(json);
      } catch (err) {
        if (err.name !== 'AbortError') setError(err);
      } finally {
        setLoading(false);
      }
    };

    fetchData();

    return () => controller.abort();
  }, [url]); // Re-run when URL changes

  return { data, error, loading };
}

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

Advanced Patterns

1. Request Deduplication

const fetchCache = new Map();

function useFetchDeduplicated(url) {
  const [state, setState] = useState({ data: null, loading: true });

  useEffect(() => {
    if (fetchCache.has(url)) {
      setState({ data: fetchCache.get(url), loading: false });
      return;
    }

    fetch(url)
      .then(res => res.json())
      .then(data => {
        fetchCache.set(url, data);
        setState({ data, loading: false });
      });
  }, [url]);

  return state;
}

2. Optimistic Updates

function TodoList() {
  const [todos, setTodos] = useState([]);

  const toggleTodo = async (id) => {
    const oldTodos = todos;
    // Optimistically update UI
    setTodos(todos.map(todo => 
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));

    try {
      await fetch(`/api/todos/${id}`, {
        method: 'PATCH',
        body: JSON.stringify({ completed: !oldTodos.find(t => t.id === id).completed })
      });
    } catch (err) {
      // Revert on error
      setTodos(oldTodos);
      showErrorToast('Failed to update');
    }
  };
}

Remember: Data fetching should be:

  1. Intentional – Not triggered by renders
  2. Controlled – With cleanup mechanisms
  3. Traceable – With proper loading/error states
  4. Efficient – Avoiding duplicate requests

Always place data fetching logic in useEffect hooks or event handlers, never directly in the render flow.

Leave a Reply

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