How to Intercept Fetch Requests in React

Intercepting native fetch calls in React applications is a foundational requirement for deterministic local development simulation. By capturing outbound network traffic at the browser level, engineering teams can decouple frontend rendering cycles from backend availability. This approach aligns with established API Mocking Fundamentals & Architecture principles, ensuring that UI components remain isolated during integration testing and rapid prototyping.

Exact Implementation Blueprint

To intercept without modifying existing components, implement a transparent proxy that wraps the native method. Follow these steps for immediate deployment:

  1. Preserve Native Reference: Capture const originalFetch = window.fetch; before any patching occurs.
  2. Define Route Matcher: Build a synchronous function evaluating input (URL/Request) and init against a centralized mock registry.
  3. Patch Global Fetch: Override window.fetch with an async wrapper.
  4. Intercept & Route: Match requests against your registry. If matched, return a synthetic Response object immediately.
  5. Delegate Fallbacks: Pass unmatched requests to originalFetch(input, init).
  6. Enforce Teardown: Always restore window.fetch = originalFetch to prevent cross-test pollution and memory leaks.
// Core Interceptor Implementation
const originalFetch = window.fetch;

window.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
 const url = new URL(input instanceof Request ? input.url : String(input), window.location.origin);
 const mockRoute = getMockForRoute(url.pathname, init?.method || 'GET');

 if (mockRoute) {
 return new Response(JSON.stringify(mockRoute.data), {
 status: mockRoute.status || 200,
 headers: { 'Content-Type': 'application/json' }
 });
 }

 return originalFetch(input, init);
};

React-Specific Integration & Lifecycle Management

In React, fetch interception must be scoped to the application lifecycle or test environment to prevent global state contamination.

  • Development Activation: Wrap the interceptor in a custom hook or top-level provider. Activate it within useEffect and return a cleanup function that restores the original fetch.
  • AbortController Compliance: Extract init.signal from intercepted requests and forward it to originalFetch. This prevents dangling promises during rapid component unmounts.
  • Concurrent Rendering Safety: Ensure mock responses resolve synchronously or with predictable microtask timing to prevent React 18+ hydration mismatches.
  • QA/Test Isolation: Inject interceptors via setupTests.ts or Playwright fixtures. This guarantees deterministic network states across parallel test runners and directly supports scalable Request Interception Patterns across CI/CD pipelines.

Targeted Fixes for Common Edge Cases

Implement these prevention strategies to avoid runtime failures in production-like environments:

  • CORS Preflight Handling: Explicitly intercept OPTIONS requests. Return 204 No Content with Access-Control-Allow-Origin, Access-Control-Allow-Methods, and Access-Control-Allow-Headers headers.
  • Streaming Responses: If mocking streaming endpoints, construct the Response body using new ReadableStream() with controller.enqueue() and controller.close().
  • Request Body Consumption: Clone the Request object (req.clone()) before calling .json() or .text() during route matching. This prevents TypeError: Body already consumed errors during fallback delegation.
  • Race Condition Deduplication: Implement a request queue keyed by URL + method. Deduplicate concurrent identical calls to prevent redundant mock evaluations and state thrashing.

Validation Checklist

Before merging, verify the following:

  • [ ] Original fetch reference is fully restored on unmount/test teardown
  • [ ] Interceptor handles concurrent React rendering without hydration warnings
  • [ ] CORS preflight (OPTIONS) returns valid 204 responses with correct headers
  • [ ] Request body cloning prevents consumption errors on fallback delegation

Properly intercepting fetch in React requires strict adherence to native API contracts and disciplined lifecycle management. By implementing a transparent wrapper, teams achieve reproducible local environments without invasive code modifications. This pattern scales effectively across complex applications, enabling robust response shaping and deterministic testing workflows.