⚛️ React Frontend Engineering • Track 09

Unit 8: Working with APIs

Architectural foundations of client-server data communications in React: REST architectural principles, HTTP semantics, Fetch API vs. Axios, complete CRUD lifecycle mutations, tri-state loading and error management, search filtering with debounce mechanics, offset pagination, and optimistic UI synchronization.

📚 Module: Unit 8 of 12 Core Units
🌐 Protocol: RESTful Architecture & HTTP/1.1–HTTP/2 Semantics
⚡ Data Engine: Fetch API, Axios & Asynchronous State Machines
📖 Format: 100% Pure Natural English (Zero LaTeX)
01

REST API Foundations & Client-Server Architecture

Modern frontend Single Page Applications (SPAs) built with React do not generate HTML on a web server per user click. Instead, the browser downloads a single bundle containing HTML, CSS, and compiled JavaScript. The user interface then executes entirely on the client machine. To display dynamic records—such as user profiles, transaction logs, e-commerce inventories, or live comments—the React client must communicate asynchronously across the Internet with remote backend services.

The dominant architectural style governing this communication is Representational State Transfer (REST), formally formulated by Roy Fielding in his 2000 doctoral dissertation. A RESTful API is not a library, framework, or protocol; it is a set of architectural constraints applied to the Web:

1. Client-Server Separation

Separates user interface concerns (state, presentation, DOM updates, user input) from data storage and business logic concerns (databases, authentication, transactions). This separation allows the React frontend and backend microservices to evolve, scale, and deploy completely independently.

2. Statelessness Constraint

Every single HTTP request dispatched from the React client to the server must contain all the contextual information required to understand and authorize the request (such as JSON Web Tokens or API keys). The server stores no client session context between round-trips.

3. Uniform Resource URIs

Resources are model entities (e.g., users, orders, products) referenced via predictable, standardized Uniform Resource Identifiers (URIs). Data is exchanged using lightweight structured formats, primarily JavaScript Object Notation (JSON).

+--------------------------------------------------------------------------+ | REST CLIENT-SERVER ASYNCHRONOUS ROUND-TRIP CYCLE | +--------------------------------------------------------------------------+ React SPA Frontend (Browser Runtime) Backend API (Node/Go/Python) +----------------------------------+ +---------------------------+ | User triggers action (e.g. View)| | Database / Business Logic| | State: { loading: true } | | | +----------------------------------+ +---------------------------+ | | | 1. HTTP GET /api/v1/products | | Headers: Authorization, Accept: json | |----------------------------------------->| | | Database Query | | Serialization | 2. HTTP 200 OK | | Headers: Content-Type: application/json | Body: [ { id: 1, name: "Laptop" } ] | |<-----------------------------------------| v v +----------------------------------+ +---------------------------+ | State: { data: [...], | | Connection closed | | loading: false } | | (Zero session state held)| | React re-renders view with data | | | +----------------------------------+ +---------------------------+

In React, interacting with a REST API involves translating external network transactions into internal component reactivity. Every network request is fundamentally asynchronous: it takes time to travel over the wire, it can succeed, and it can catastrophically fail due to hardware faults, network dropouts, or authorization rejections.

02

HTTP Protocol Fundamentals, Method Semantics & Status Codes

RESTful communications operate directly over the Hypertext Transfer Protocol (HTTP). To interact with APIs reliably, frontend engineers must understand the strict semantic meaning of HTTP request methods, headers, and status code categories.

HTTP Request & Response Architecture

An HTTP transaction consists of two coordinated messages:

HTTP Method Semantics: Safety and Idempotence

HTTP methods are defined by two crucial theoretical properties established by the Internet Engineering Task Force (IETF):

Method Primary Role Safe? Idempotent? Payload Body? Target URI Example
GET Retrieve resource representations Yes Yes No /api/v1/users/42
POST Create new subordinate resources No No Yes /api/v1/users
PUT Completely replace target resource No Yes Yes /api/v1/users/42
PATCH Apply partial delta modification No Depends Yes /api/v1/users/42
DELETE Remove target resource permanently No Yes Rarely /api/v1/users/42

HTTP Status Code Categories

Success & Redirection Ranges
  • 200 OK: Standard success status for GET, PUT, or PATCH requests returning data.
  • 201 Created: Resource successfully created on server (standard POST response). Usually accompanied by a Location header.
  • 204 No Content: Action succeeded, but response body is intentionally empty (standard DELETE response).
  • 304 Not Modified: Cached client representation is still valid (ETag match); saves network bandwidth.
Client & Server Error Ranges
  • 400 Bad Request: Malformed request syntax or client payload validation failure.
  • 401 Unauthorized: Missing or expired authentication credentials.
  • 403 Forbidden: Identity known, but client lacks permission for the resource.
  • 404 Not Found: Target resource URI does not exist on the server.
  • 422 Unprocessable Entity: Syntax valid, but semantic business validation failed.
  • 500 Internal Server Error: Unhandled backend exception or database failure.
+--------------------------------------------------------------------------+ | HTTP STATUS CODE EVALUATION DECISION TREE | +--------------------------------------------------------------------------+ HTTP Response Received | +---------------+---------------+ | | Status 200–299 Status 400–599 (Success Range) (Error Range) | | +-----------+-----------+ +-----------+-----------+ | | | | 200 / 201 204 401 / 403 404 / 422 / 500 Parse JSON Body No Body Content Redirect to Login Display Error Banner Update UI State Update UI List or Refresh Token Enable Retry Button
03

The Native Fetch API vs. Axios: Architectural Comparison

In the JavaScript and React ecosystem, developers primarily choose between two HTTP clients: the browser's native Fetch API and the third-party Axios promise-based library. Both fulfill the same fundamental purpose, but their underlying mechanics, error-handling conventions, and developer ergonomics differ significantly.

Feature / Dimension Browser Native Fetch API Axios Library
Installation & Overhead Zero external dependencies (native browser standard). Requires npm package install (npm install axios, ~13KB gzipped).
HTTP Error Handling Does NOT reject on 4xx/5xx status codes. Requires manual response.ok verification. Automatically rejects the promise whenever HTTP status falls outside the 2xx range.
JSON Data Serialization Manual two-step process: requires calling response.json() and JSON.stringify(body). Automatic transformation: transforms objects to JSON and parses responses automatically in response.data.
Request Cancellation Supported via standard AbortController and signal property. Supported via native AbortController and legacy CancelTokens.
Global Interceptors Not supported natively; requires custom wrapper abstractions. Built-in request and response interceptors (ideal for automatic auth token injection).
Request Timeout No native timeout config; requires AbortSignal.timeout(ms). Direct configuration property: { timeout: 5000 }.
🚨 The Critical Fetch API Error Handling Trap

The most prevalent bug in React API integration occurs when developers assume fetch() will trigger the catch block on a 404 Not Found or 500 Server Error.

Mechanical Truth: The Promise returned by fetch() only rejects on network failure (e.g., DNS lookup failure, complete loss of Internet connection, or CORS preflight rejection). If the server responds with a 404, 401, or 500 status code, the Promise resolves successfully! Developers must explicitly verify the boolean response.ok property (which is true only for statuses 200–299) before reading data.

Comparative Syntax: Native Fetch vs Axios
// 1. NATIVE BROWSER FETCH API (Manual ok verification & two-step JSON parsing)
async function fetchWithNative(url) {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      // Mandatory manual check: fetch resolves even on 404/500!
      throw new Error(`HTTP Error: ${response.status} ${response.statusText}`);
    }
    const data = await response.json(); // Explicit parsing step required
    return data;
  } catch (error) {
    console.error('Fetch operation failed:', error.message);
    throw error;
  }
}

// 2. AXIOS LIBRARY (Automatic rejection on non-2xx & automatic JSON parsing)
import axios from 'axios';

async function fetchWithAxios(url) {
  try {
    // Automatically rejects on 4xx/5xx status codes
    const response = await axios.get(url, { timeout: 5000 });
    return response.data; // Data is pre-parsed and immediately available
  } catch (error) {
    if (error.response) {
      // Server responded with status code outside 2xx
      console.error(`Server Error: ${error.response.status}`, error.response.data);
    } else if (error.request) {
      // Request dispatched but no response received (network drop)
      console.error('Network Error: No response received');
    } else {
      console.error('Configuration Error:', error.message);
    }
    throw error;
  }
}
04

The Asynchronous Tri-State Lifecycle: Loading, Error & Success

In React, an API request cannot be evaluated as an instantaneous transaction. It represents an ongoing asynchronous journey with distinct operational states. A robust, production-grade user interface must explicitly handle every possible stage of this journey:

1. Loading State

The period between request dispatch and response arrival. The UI must convey activity to the user through spinner indicators, disabled buttons, or skeleton placeholder screens to prevent duplicate submissions and visual jarring.

2. Error State

When network dropouts, 4xx client rejections, or 5xx server exceptions occur. The UI must catch the failure gracefully, display human-readable diagnostics, and offer actionable recovery mechanisms (such as a "Retry Request" button).

3. Success / Data State

The payload has arrived, passed validation, and is stored in state. The UI renders the data, taking into account edge cases such as empty lists (data.length === 0) with informative empty-state prompts.

The Boolean Flags Anti-Pattern vs. Finite State Machines

A frequent anti-pattern in React development is declaring independent, uncoordinated boolean flags:
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);

Because these booleans can technically be set in any combination, components fall victim to impossible states—for example, a bug where both isLoading === true and isError === true simultaneously, resulting in a UI that displays both a loading spinner and an error banner at the same time.

The professional architectural solution is to represent status as an enumerated state machine:
const [status, setStatus] = useState('idle'); // 'idle' | 'loading' | 'success' | 'error'

+--------------------------------------------------------------------------+ | FINITE STATE MACHINE ASYNCHRONOUS UI TRANSITIONS | +--------------------------------------------------------------------------+ [ IDLE ] | Component Mounts / User Action | v [ LOADING ] (Show Skeletons) | +--------------+--------------+ | | Fetch Resolves Fetch Rejects / Status 200 OK Status 4xx/5xx | | v v [ SUCCESS ] [ ERROR ] (Render Data List) (Show Alert Banner & | "Retry" Button) | | +--------------+--------------+ | User Retries | v [ LOADING ]
05

Reading & Displaying API Data: GET Requests in the Component Lifecycle

Executing an HTTP GET request to populate a component with external data requires orchestrating state initialization, side effect execution via useEffect, and declarative JSX rendering.

Key Engineering Principles for Displaying Data

Production GET Request: Tri-State Lifecycle & Defensive Rendering
import { useState, useEffect } from 'react';

function UserDirectory() {
  const [users, setUsers] = useState([]);
  const [status, setStatus] = useState('loading'); // 'loading' | 'success' | 'error'
  const [errorMessage, setErrorMessage] = useState('');

  const loadUsers = async () => {
    setStatus('loading');
    setErrorMessage('');

    try {
      const response = await fetch('https://jsonplaceholder.typicode.com/users');
      if (!response.ok) {
        throw new Error(`Failed to load users: HTTP ${response.status}`);
      }
      const data = await response.json();
      setUsers(data);
      setStatus('success');
    } catch (err) {
      setErrorMessage(err.message || 'An unexpected network error occurred.');
      setStatus('error');
    }
  };

  useEffect(() => {
    loadUsers();
  }, []);

  // 1. Loading UI State
  if (status === 'loading') {
    return <div className="card"><p>Fetching directory records from server...</p></div>;
  }

  // 2. Error UI State with Actionable Recovery
  if (status === 'error') {
    return (
      <div className="callout danger">
        <div className="callout-title">Network Request Error</div>
        <p>{errorMessage}</p>
        <button onClick={loadUsers}>Retry Request</button>
      </div>
    );
  }

  // 3. Empty Data State
  if (users.length === 0) {
    return <div className="card"><p>No active user records found.</p></div>;
  }

  // 4. Success UI State: Mapping over data with stable keys
  return (
    <div className="card">
      <h3>Active Users Directory ({users.length})</h3>
      <ul style={{ paddingLeft: '20px' }}>
        {users.map((user) => (
          <li key={user.id} style={{ marginBottom: '8px' }}>
            <strong>{user.name}</strong> • {user.email} 
            <span style={{ color: '#64748b', fontSize: '11px' }}> ({user.company?.name ?? 'Independent'})</span>
          </li>
        ))}
      </ul>
    </div>
  );
}
06

Complete CRUD Mutations: POST, PUT, PATCH & DELETE

While GET requests are read-only operations, CRUD (Create, Read, Update, Delete) applications demand active server data mutations. Mutations require sending payload representations over the wire and updating the local client state to reflect the server's changes.

1. POST: Creating New Resources

Dispatched when submitting a form to create a new record. The request must declare the Content-Type: application/json header and serialize the JavaScript state into a JSON string using JSON.stringify(). The server processes the payload, generates an authoritative record identifier (such as an auto-incrementing ID or UUID), and returns the created object with an HTTP 201 Created status.

2. PUT vs. PATCH: The Modification Dilemma

PUT: Complete Replacement

PUT replaces the entire target entity. If a user record has ten attributes and you submit a PUT payload containing only { name: "Bob" }, the server will overwrite the record, effectively erasing the remaining nine fields or resetting them to default nulls.

PATCH: Partial Delta Mutation

PATCH applies a targeted partial update. Submitting { email: "new@acme.com" } via PATCH instructs the server to update only the email attribute, leaving all other fields intact. In modern frontend development, PATCH is generally preferred for form editing.

3. DELETE: Resource Destruction

Instructs the server to permanently remove the target resource at the specified URI (e.g., DELETE /api/posts/15). A successful deletion typically returns 204 No Content (or 200 OK with a status confirmation).

+--------------------------------------------------------------------------+ | COMPLETE RESTFUL CRUD PROTOCOL FLOW & CLIENT SYNC | +--------------------------------------------------------------------------+ [CREATE (POST /api/items)] Client: POST { title: "New Item" } Server: 201 Created { id: 105, title: "New Item" } Client State: setItems(prev => [...prev, newItem]) [READ (GET /api/items/105)] Client: GET /api/items/105 Server: 200 OK { id: 105, title: "New Item" } Client State: Sets active view model [UPDATE (PATCH /api/items/105)] Client: PATCH { title: "Updated Item" } Server: 200 OK { id: 105, title: "Updated Item" } Client State: setItems(prev => prev.map(item => item.id === 105 ? updated : item)) [DELETE (DELETE /api/items/105)] Client: DELETE /api/items/105 Server: 204 No Content Client State: setItems(prev => prev.filter(item => item.id !== 105))
Modular CRUD Service Implementation
const BASE_URL = 'https://jsonplaceholder.typicode.com/posts';

// Standardized headers for JSON transactions
const JSON_HEADERS = {
  'Content-Type': 'application/json',
  'Accept': 'application/json'
};

export const PostService = {
  // CREATE
  async create(newPost) {
    const res = await fetch(BASE_URL, {
      method: 'POST',
      headers: JSON_HEADERS,
      body: JSON.stringify(newPost)
    });
    if (!res.ok) throw new Error(`POST failed: ${res.status}`);
    return res.json();
  },

  // READ ALL
  async getAll() {
    const res = await fetch(BASE_URL);
    if (!res.ok) throw new Error(`GET failed: ${res.status}`);
    return res.json();
  },

  // PARTIAL UPDATE (PATCH)
  async patch(id, partialUpdates) {
    const res = await fetch(`${BASE_URL}/${id}`, {
      method: 'PATCH',
      headers: JSON_HEADERS,
      body: JSON.stringify(partialUpdates)
    });
    if (!res.ok) throw new Error(`PATCH failed: ${res.status}`);
    return res.json();
  },

  // DELETE
  async remove(id) {
    const res = await fetch(`${BASE_URL}/${id}`, {
      method: 'DELETE'
    });
    if (!res.ok) throw new Error(`DELETE failed: ${res.status}`);
    return true; // Successfully deleted
  }
};
07

Search, Filtering & Asynchronous Debouncing Mechanics

A core requirement of data-intensive web applications is enabling users to search and filter datasets. Filtering can be executed in two architectural locations:

The Keystroke Flooding Problem

When binding an input field's onChange event directly to a server-side API call, an average user typing a 10-character word will dispatch 10 distinct HTTP requests in under two seconds. This causes keystroke flooding: overwhelming backend servers, consuming mobile bandwidth, and creating race conditions where asynchronous responses resolve out of order.

The Debouncing Solution

Debouncing is a programming pattern that limits the rate at which a function executes. When a debounced function is invoked, it resets a timer delay (e.g., 400ms). The actual function execution occurs only after the user has completely stopped typing for the duration of that delay.

+--------------------------------------------------------------------------+ | KEYSTROKE FLOODING VS DEBOUNCED API DISPATCH | +--------------------------------------------------------------------------+ WITHOUT DEBOUNCING (Disastrous Keystroke Flooding): User types: 'R' 'e' 'a' 'c' 't' HTTP Calls: GET?q=R GET?q=Re GET?q=Rea GET?q=Reac GET?q=React Result: 5 network requests in 600ms! (Overwhelms server & causes race conditions) WITH DEBOUNCING (400ms Timer Reset): User types: 'R' ==> Starts 400ms Timer... T+100ms: 'e' ==> CLEARS previous timer! Starts new 400ms Timer... T+200ms: 'a' ==> CLEARS previous timer! Starts new 400ms Timer... T+300ms: 'c' ==> CLEARS previous timer! Starts new 400ms Timer... T+400ms: 't' ==> CLEARS previous timer! Starts new 400ms Timer... T+800ms: (User pauses typing for 400ms) | v TIMER EXPIRES: Single HTTP Request dispatched: GET /api/search?q=React Result: Exactly 1 clean, authoritative network request!
Debounced Search Input Hook Implementation
import { useState, useEffect } from 'react';

// Custom hook to debounce any rapidly changing value
export function useDebounce(value, delay = 400) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    // 1. Establish timer to update value after delay
    const timer = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    // 2. Clear timer if value changes before delay expires (user is still typing)
    return () => {
      clearTimeout(timer);
    };
  }, [value, delay]);

  return debouncedValue;
}

// Consuming Search Component
function LiveProductSearch() {
  const [searchTerm, setSearchTerm] = useState('');
  const [results, setResults] = useState([]);
  const [isSearching, setIsSearching] = useState(false);

  // Debounced search query updates 400ms after user stops typing
  const debouncedQuery = useDebounce(searchTerm, 400);

  useEffect(() => {
    if (!debouncedQuery.trim()) {
      setResults([]);
      return;
    }

    const searchApi = async () => {
      setIsSearching(true);
      try {
        const res = await fetch(`https://jsonplaceholder.typicode.com/posts?title_like=${encodeURIComponent(debouncedQuery)}`);
        const data = await res.json();
        setResults(data);
      } catch (err) {
        console.error('Search query failed:', err);
      } finally {
        setIsSearching(false);
      }
    };

    searchApi();
  }, [debouncedQuery]); // Re-runs strictly when the debounced value changes

  return (
    <div className="card">
      <input
        type="text"
        placeholder="Search posts by title..."
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
        style={{ width: '100%', padding: '8px', borderRadius: '6px', border: '1px solid #cbd5e1' }}
      />
      {isSearching && <p style={{ fontSize: '12px', color: '#0284c7' }}>Querying server...</p>}
      <ul style={{ marginTop: '12px', paddingLeft: '20px' }}>
        {results.map((item) => (
          <li key={item.id}>{item.title}</li>
        ))}
      </ul>
    </div>
  );
}
08

Pagination Architectures: Offset/Limit vs. Cursor-Based Models

When databases contain thousands or millions of records, transferring the entire dataset over HTTP in a single response causes server memory exhaustion, excessive network latency, and severe DOM rendering slowdowns. Pagination segments large datasets into discrete, manageable subsets.

1. Offset/Limit (Page-Based) Pagination

The most common pagination strategy. The client supplies two parameters via query strings:
/api/products?page=3&limit=10 (or offset=20&limit=10).

2. Cursor-Based (Keyset) Pagination

Instead of a numeric offset, the server returns an opaque pointer or timestamp corresponding to the last item on the page:
/api/feed?cursor=eyJpZCI6MTA0Mn0=&limit=20.

+--------------------------------------------------------------------------+ | OFFSET PAGINATION STATE MANAGEMENT PIPELINE | +--------------------------------------------------------------------------+ Current State: { page: 2, limit: 10 } | User clicks "Next Page" button | v State Update: setPage(prev => prev + 1) // page becomes 3 | v useEffect triggers because [page] is in dependency array | v Dispatches HTTP: GET /api/posts?_page=3&_limit=10 | v Server returns 10 items + Total Count Header (X-Total-Count: 100) | v React updates state: - items: newRecords - totalPages: Math.ceil(100 / 10) = 10 - UI renders Page 3 table with active pagination controls
Paginated Data Table with Page Controls
import { useState, useEffect } from 'react';

function PaginatedPostTable() {
  const [posts, setPosts] = useState([]);
  const [page, setPage] = useState(1);
  const [isLoading, setIsLoading] = useState(true);
  const limit = 5;
  const totalItems = 50; // Mock total count (normally supplied by X-Total-Count header)
  const totalPages = Math.ceil(totalItems / limit);

  useEffect(() => {
    const fetchPage = async () => {
      setIsLoading(true);
      try {
        const res = await fetch(`https://jsonplaceholder.typicode.com/posts?_page=${page}&_limit=${limit}`);
        const data = await res.json();
        setPosts(data);
      } catch (err) {
        console.error('Pagination fetch error:', err);
      } finally {
        setIsLoading(false);
      }
    };

    fetchPage();
  }, [page]); // Re-fetch automatically whenever page changes

  return (
    <div className="card">
      <h3>Posts Catalog • Page {page} of {totalPages}</h3>
      {isLoading ? (
        <p>Loading page records...</p>
      ) : (
        <ul style={{ paddingLeft: '20px', minHeight: '160px' }}>
          {posts.map((post) => (
            <li key={post.id} style={{ marginBottom: '6px' }}>
              <strong>#{post.id}</strong> {post.title}
            </li>
          ))}
        </ul>
      )}

      {/* Pagination Controls */}
      <div style={{ display: 'flex', gap: '8px', alignItems: 'center', marginTop: '16px' }}>
        <button onClick={() => setPage((p) => Math.max(1, p - 1))} disabled={page === 1}>
          &larr; Previous
        </button>
        <span style={{ fontSize: '12px' }}>Page {page}</span>
        <button onClick={() => setPage((p) => Math.min(totalPages, p + 1))} disabled={page === totalPages}>
          Next &rarr;
        </button>
      </div>
    </div>
  );
}
09

Optimistic UI Updates vs. Pessimistic UI Updates

When performing data mutations (such as deleting a comment, starring an item, or toggling a task), developers face a key architectural choice between two user experience paradigms:

1. Pessimistic UI Updates (Default)

The application waits for the remote server to return a successful response before updating the local client UI.

  • Pros: Guarantees perfect consistency between the client display and server database.
  • Cons: Introduces latency lag. The user experiences an interface freeze or spinner for 200–800ms on every click.
2. Optimistic UI Updates

The application updates the local client UI immediately assuming the server request will succeed, while dispatching the network call in the background.

  • Pros: Zero apparent latency; the interface feels instantaneous and native.
  • Cons: Requires complex rollback logic. If the server request fails, the UI must revert to its previous state and notify the user.
+--------------------------------------------------------------------------+ | OPTIMISTIC UI MUTATION & ROLLBACK LIFECYCLE | +--------------------------------------------------------------------------+ User clicks "Delete Item #42" | v 1. SNAPSHOT: Save backup of current state: const previousItems = [...items] 2. OPTIMISTIC UPDATE: Remove item #42 from local state instantly (0ms latency!) 3. DISPATCH: Send DELETE /api/items/42 across network | +-----------------------+-----------------------+ | | Server returns 204 OK Server returns 500 Error | | v v Mutation Confirmed! ROLLBACK EXECUTED! Discard backup snapshot. setItems(previousItems) UI remains in updated state. Display error toast notification.
Optimistic Deletion with Automatic Rollback
import { useState } from 'react';

function TaskListManager() {
  const [tasks, setTasks] = useState([
    { id: 1, title: 'Audit API contracts' },
    { id: 2, title: 'Configure Axios interceptors' },
    { id: 3, title: 'Implement debounce hook' }
  ]);
  const [errorMessage, setErrorMessage] = useState('');

  const handleDeleteOptimistic = async (idToDelete) => {
    // 1. Capture snapshot for rollback
    const backupSnapshot = [...tasks];

    // 2. Optimistic UI Update: remove from local state immediately
    setTasks((prev) => prev.filter((t) => t.id !== idToDelete));
    setErrorMessage('');

    try {
      const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${idToDelete}`, {
        method: 'DELETE'
      });

      if (!response.ok) {
        throw new Error('Server rejected deletion request.');
      }
      // Success: Server confirmed deletion, no further action required
    } catch (err) {
      // 3. Rollback Phase: Restore previous state snapshot on network failure
      setTasks(backupSnapshot);
      setErrorMessage(`Failed to delete task. Changes reverted. (${err.message})`);
    }
  };

  return (
    <div className="card">
      <h3>Optimistic Task Removal</h3>
      {errorMessage && <p style={{ color: 'red', fontSize: '12px' }}>{errorMessage}</p>}
      <ul style={{ paddingLeft: '20px' }}>
        {tasks.map((task) => (
          <li key={task.id} style={{ marginBottom: '8px' }}>
            {task.title}{' '}
            <button onClick={() => handleDeleteOptimistic(task.id)} style={{ color: '#ef4444' }}>
              Delete Instantly
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}
10

Technical Interview & Academic Exam Questions

Q1: Why does the native browser Fetch API not reject its Promise on HTTP 404 or 500 status codes?

Answer: By specification, the Fetch API Promise only rejects when an actual network failure occurs (such as an offline browser, a DNS resolution failure, or a blocked CORS preflight request). A 404 Not Found or 500 Internal Server Error is considered a valid, completed HTTP communication between client and server. To handle HTTP errors, developers must manually verify response.ok (which checks whether the status code lies within the 200–299 range) before consuming the payload.

Q2: What is the semantic difference between HTTP PUT and HTTP PATCH?

Answer: PUT represents a complete replacement of the target resource. The client is expected to transmit the full representation of the object; any omitted fields may be overwritten with null or default values by the server. PATCH represents a partial modification (delta update). The client transmits only the specific attributes that must be changed, leaving all other properties untouched.

Q3: What is idempotence in HTTP, and which REST methods are idempotent?

Answer: An HTTP method is idempotent if executing it multiple times consecutively with identical parameters yields the exact same server state as executing it once. GET, HEAD, PUT, and DELETE are idempotent (deleting resource #5 once or five times leaves resource #5 deleted). POST is NOT idempotent (dispatching five identical POST requests will create five separate duplicate records in the database).

Q4: Why is debouncing necessary when implementing live search inputs in React?

Answer: Without debouncing, an API request is dispatched on every single keystroke. In addition to overwhelming backend servers and wasting network bandwidth, this causes severe network race conditions: earlier, slower requests for shorter query strings can arrive after later requests, overwriting the UI with stale search results. Debouncing delays the API invocation until the user has stopped typing for a defined threshold (e.g., 400ms).

Q5: What is the "impossible state" problem when managing asynchronous requests with booleans?

Answer: When independent booleans are used (e.g., [isLoading, isError, isSuccess]), improper state updates can cause contradictory conditions where both isLoading === true and isError === true simultaneously. This results in visual glitches where a spinner and error banner appear concurrently. Replacing independent booleans with a single enumerated status string ('idle' | 'loading' | 'success' | 'error') enforces a strict finite state machine.

Q6: What is "page drift" in offset-based pagination and how does cursor pagination solve it?

Answer: In offset pagination (?page=2&limit=10), the database calculates an offset: OFFSET 10. If a new item is inserted on page 1 while a user is navigating to page 2, all existing rows shift down by one. As a result, the tenth item from page 1 shifts to index 11 and appears a second time on page 2 (duplicate record). Cursor-based pagination solves this by querying against a unique, sequential pointer (e.g., WHERE id < lastSeenId), which remains unaffected by insertions ahead of the cursor.

Q7: How do Axios interceptors function and what are their primary use cases in React?

Answer: Interceptors are middleware functions that Axios executes before a request is transmitted (Request Interceptors) or before a response is handed over to the calling promise handler (Response Interceptors). They are primarily used for: automatically injecting Bearer JWT tokens into the Authorization header, globally refreshing expired access tokens on 401 responses, and logging telemetry metrics.

Q8: How does an Optimistic UI update work, and what must occur if the network request fails?

Answer: An optimistic update immediately modifies the client state and UI to reflect the intended mutation before transmitting the HTTP request across the wire, creating an instant user experience. If the server request fails, the application must execute a rollback: restoring the UI state from a saved snapshot taken prior to the mutation and displaying a descriptive error alert to the user.

Q9: Why must you avoid using array indices as keys when mapping over API data in React?

Answer: React uses the key prop to track the identity of elements across render cycles during Virtual DOM reconciliation. If array indices are used as keys, deleting or reordering items shifts all subsequent indices. React will mismatch DOM nodes and internal component state (such as inputs or animations), leading to subtle visual and functional bugs. Always use immutable backend identifiers (such as item.id).

Q10: What is CORS, and why does a CORS error occur on the client?

Answer: Cross-Origin Resource Sharing (CORS) is a browser security mechanism that restricts web applications from making HTTP requests to a domain, protocol, or port different from the one that served the web page. A CORS error occurs when the backend server fails to respond with the appropriate Access-Control-Allow-Origin headers during the HTTP transaction or preflight OPTIONS check. CORS cannot be fixed in client-side React code; it must be configured on the server.

11

Unit Summary & API Engineering Reference Cheatsheet

Operation HTTP Method Expected Status React State Protocol Core Engineering Rule
Read All GET /items 200 OK setItems(data) Check response.ok; handle empty array (data.length === 0).
Read Single GET /items/:id 200 OK setItem(data) Use optional chaining (item?.prop) to prevent null dereference.
Create POST /items 201 Created setItems(prev => [...prev, res]) Set Content-Type: application/json and JSON.stringify(body).
Update (Full) PUT /items/:id 200 OK setItems(prev => prev.map(...)) Must transmit entire entity; omitted fields may be erased by server.
Update (Delta) PATCH /items/:id 200 OK setItems(prev => prev.map(...)) Transmit only modified fields; preserves untouched attributes.
Delete DELETE /items/:id 204 No Content setItems(prev => prev.filter(...)) Snapshot previous state before deleting if applying optimistic UI.
Core Architectural Principles to Retain
  • Fetch Does Not Reject on 4xx/5xx: Always verify response.ok manually when utilizing native fetch().
  • Model States as State Machines: Never use uncoordinated boolean flags (isLoading, isError). Use enumerated status strings to prevent impossible UI states.
  • Debounce User Typing: Always delay live search queries by 300–500ms to eliminate keystroke flooding and avoid network race conditions.
  • Paginate Early: Large datasets must be segmented using offset/limit or cursor pagination to maintain database performance and lightweight DOM trees.