How to Use React Fetch with the Fetch API and Console Log in React JS

Use the useEffect hook to trigger fetch requests after component mount, store results in state with useState, and log responses to the console before updating the UI.

The facebook/react repository implements a declarative rendering engine where side effects such as network requests belong inside the useEffect hook. Mastering the correct react fetch pattern ensures your components work reliably in both classic and concurrent rendering modes while keeping your debugging output intact.

Why useEffect is the Correct Place for React Fetch Requests

React’s architecture separates what the UI should look like from when changes occur. The component describes the UI for a given state, while React decides when to reconcile those changes. Side effects—operations that reach outside the component such as fetch—must run after the component commits to the DOM.

The useEffect hook schedules these side effects to run after the render phase. According to the source code in [packages/react/src/ReactHooks.js](https://github.com/facebook/react/blob/main/packages/react/src/ReactHooks.js#L87), the public API delegates to the reconciler, while the low-level implementation in [packages/react-reconciler/src/ReactFiberHooks.js](https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberHooks.js#L2670) ties the effect into the fiber scheduler.

The useEffect Lifecycle and Fiber Architecture

When a component mounts, React schedules the effect callback. On subsequent renders, React re-runs the callback only if one of its dependency values changes. When the component unmounts, any cleanup function returned from the callback executes.

Because React may pause, abort, or restart rendering in Concurrent Mode, effects must be idempotent. Declaring the async fetch inside useEffect and using an AbortController for cancellation ensures that pending network calls do not attempt state updates on unmounted components.

Basic React Fetch Example with Console Logging

The following pattern runs a single fetch when the component mounts, logs the result to the console, and stores the data in local state.

import React, {useEffect, useState} from 'react';

function UsersList() {
  const [users, setUsers] = useState<Array<any>>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    // Effect runs after the first render (mount)
    fetch('https://jsonplaceholder.typicode.com/users')
      .then(res => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res.json();
      })
      .then(data => {
        console.log('Fetched users →', data);   // Console-log the result
        setUsers(data);
        setLoading(false);
      })
      .catch(err => {
        console.error('Fetch error →', err);
        setError(err.message);
        setLoading(false);
      });
  }, []); // Empty array → run only once (on mount)

  if (loading) return <p>Loading…</p>;
  if (error)   return <p style={{color: 'red'}}>Error: {error}</p>;

  return (
    <ul>
      {users.map(u => (
        <li key={u.id}>{u.name}</li>
      ))}
    </ul>
  );
}

The empty dependency array ensures the fetch runs only on mount. The console output appears in the browser developer tools, allowing you to inspect the raw API response before it reaches the UI.

Handling Async/Await and Cleanup with AbortController

For components that fetch data based on dynamic props, or when you need to cancel pending requests to prevent memory leaks, combine async/await with an AbortController.

import React, {useEffect, useState} from 'react';

function Post({postId}: {postId: number}) {
  const [post, setPost] = useState<any>(null);
  const [status, setStatus] = useState<'idle' | 'loading' | 'error'>('idle');

  useEffect(() => {
    const controller = new AbortController();          // Cancellation token
    const signal = controller.signal;

    async function load() {
      setStatus('loading');
      try {
        const response = await fetch(
          `https://jsonplaceholder.typicode.com/posts/${postId}`,
          {signal}
        );
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        const data = await response.json();
        console.log('Fetched post →', data);
        setPost(data);
        setStatus('idle');
      } catch (e) {
        if (e.name === 'AbortError') {
          console.log('Fetch aborted');
        } else {
          console.error('Fetch failed →', e);
          setStatus('error');
        }
      }
    }

    load();

    // Cleanup runs when the component unmounts or postId changes
    return () => controller.abort();
  }, [postId]); // Re-run when the requested post changes

  if (status === 'loading') return <p>Loading post…</p>;
  if (status === 'error')   return <p>Failed to load post.</p>;
  if (!post)                return null;

  return (
    <article>
      <h2>{post.title}</h2>
      <p>{post.body}</p>
    </article>
  );
}

The cleanup function returned from useEffect aborts any in-flight request when the component unmounts or when the postId prop changes, preventing "setState on unmounted component" warnings.

Preventing Memory Leaks in Concurrent Mode

React’s Concurrent Mode may pause, abort, or restart rendering. Effects must therefore be idempotent and safe to cancel. The AbortController pattern ensures that pending network calls do not attempt state updates after a component has been removed from the tree, aligning with the fiber architecture implemented in [ReactFiberHooks.js](https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberHooks.js#L2670).

Creating a Reusable React Fetch Hook

Abstract the pattern into a custom hook to share fetch logic across components while maintaining console logging for debugging.

import {useEffect, useState} from 'react';

export function useFetch<T>(url: string) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    const controller = new AbortController();
    fetch(url, {signal: controller.signal})
      .then(r => {
        if (!r.ok) throw new Error(`HTTP ${r.status}`);
        return r.json() as Promise<T>;
      })
      .then(json => {
        console.log('useFetch →', json);
        setData(json);
        setLoading(false);
      })
      .catch(err => {
        if (err.name !== 'AbortError') {
          console.error('useFetch error →', err);
          setError(err);
          setLoading(false);
        }
      });
    return () => controller.abort();
  }, [url]);

  return {data, loading, error};
}

Components can now call const {data, loading, error} = useFetch<User[]>('/api/users'); and receive automatic console logging for every request.

Key React Source Files for Fetch Implementation

Understanding where useEffect lives in the React codebase clarifies why these patterns are safe across rendering modes.

File Purpose
[packages/react/src/ReactHooks.js](https://github.com/facebook/react/blob/main/packages/react/src/ReactHooks.js#L87) Defines the public useEffect API that schedules side-effects after render.
[packages/react-reconciler/src/ReactFiberHooks.js](https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberHooks.js#L2670) Low-level implementation tying hooks into the fiber scheduler; essential for understanding cleanup and concurrency safety.
[packages/react-dom/src/client/ReactDOMRoot.js](https://github.com/facebook/react/blob/main/packages/react-dom/src/client/ReactDOMRoot.js) Demonstrates how roots are created and when effects are flushed to the DOM.
[packages/scheduler/src/Scheduler.js](https://github.com/facebook/react/blob/main/packages/scheduler/src/Scheduler.js) Powers React’s concurrent mode; explains why effects must be side-effect-only and idempotent.
[packages/react-server/src/ReactFizzHooks.js](https://github.com/facebook/react/blob/main/packages/react-server/src/ReactFizzHooks.js) Server-side hook implementation showing useEffect is a no-op on the server, reinforcing the client-only nature of fetch.

These files collectively describe where the useEffect hook lives, how it is scheduled, and why the patterns shown above remain safe across both classic and concurrent rendering modes.

Summary

  • Place every react fetch call inside useEffect to ensure it runs after the component commits to the DOM.
  • Use an empty dependency array ([]) for one-time fetches, or include specific dependencies to refetch when props change.
  • Always implement AbortController cleanup to prevent memory leaks and "setState on unmounted component" warnings in Concurrent Mode.
  • Log responses immediately after the fetch resolves but before calling setState to debug data shapes without triggering extra renders.
  • Abstract reusable logic into a custom hook (e.g., useFetch) to standardize error handling, loading states, and console logging across your application.

Frequently Asked Questions

Where should I place my fetch call in a React component?

Always place fetch calls inside the useEffect hook. According to the implementation in [packages/react/src/ReactHooks.js](https://github.com/facebook/react/blob/main/packages/react/src/ReactHooks.js#L87), this ensures the network request runs after the component has committed to the DOM, preventing SSR issues and ensuring the component is ready to receive data.

How do I prevent console logging on every render?

Move the console.log statement inside the useEffect callback or inside the .then() chain of your fetch promise, and ensure your dependency array is correctly configured. If you log inside the render body itself, it will execute on every render cycle. By logging only after the fetch resolves, you see output exactly once per network request.

Why do I need AbortController with react fetch?

React’s Concurrent Mode may pause, abort, or restart rendering, and components can unmount before a fetch completes. The AbortController provides a cleanup mechanism that cancels in-flight requests when the component unmounts or dependencies change, preventing attempts to update state on unmounted components. This aligns with the fiber architecture detailed in [packages/react-reconciler/src/ReactFiberHooks.js](https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberHooks.js#L2670).

Can I use async/await directly in useEffect?

You cannot declare the useEffect callback itself as async because it must return either nothing or a cleanup function, not a Promise. Instead, define an async function inside the effect (e.g., async function load() { ... }) and then invoke it immediately. This pattern maintains the correct cleanup semantics while allowing you to use await for cleaner asynchronous code.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s https://instagit.com/install.md

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client