Learn ReactJS - API Integration & Error Handling
Episode 13 of 24

Learn ReactJS - API Integration & Error Handling

This episode teaches consuming REST and GraphQL APIs, fetch policies with retries and retry delays, centralized error handling complete with fallback UI, plus preloading data and optimistic updates for a better user experience.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

Poor API integration is felt in the user's hands: a spinner spinning forever, requests failing without explanation, or lists out of sync after an action. Episode 13 makes your integration robust.

We start with consuming REST and GraphQL, then discuss fetch policies with retries and retry delays, centralized error handling with good fallback UI, and finish with two advanced UX patterns: preloading data and optimistic updates.

Consuming REST API and GraphQL API

REST with a Separate API Layer

Separate the API logic from components by creating your own module. This makes reuse and testing easier:

JSSeparate API layer
// src/api/users.js
export async function getUsers() {
  const res = await fetch("https://api.example.com/users")
  if (!res.ok) throw new Error(`HTTP ${res.status}`)
  return res.json()
}
 
// komponen memanggil
import { getUsers } from "../api/users.js"
const users = await getUsers()

export async function getUsers() isolates the endpoint details in one place. Components only know that this function returns an array of users or throws an error.

GraphQL with gql

For GraphQL, queries are built with the query language and sent via HTTP POST. Use a library like urql or @apollo/client:

JSGraphQL query with urql
import { useQuery, gql } from "urql"
 
const USERS_QUERY = gql`
  query {
    users {
      id
      name
    }
  }
`
 
function DaftarUser() {
  const [result] = useQuery({ query: USERS_QUERY })
  if (result.fetching) return <p>Memuat...</p>
  if (result.error) return <p>Error: {result.error.message}</p>
  return <ul>{result.data.users.map((u) => <li key={u.id}>{u.name}</li>)}</ul>
}

useQuery({ query: USERS_QUERY }) runs the GraphQL query and provides fetching, error, and data. GraphQL's advantage: the client only fetches the fields it actually needs.

Fetch Policies, Retries, and Retry Delays

Automatic Retries with TanStack Query

Networks can't be relied on. Retries with exponential backoff turn temporarily failing requests into automatic successes:

JSRetry with TanStack Query
import { useQuery } from "@tanstack/react-query"
 
function DaftarUser() {
  const { data, error, refetch } = useQuery({
    queryKey: ["users"],
    queryFn: getUsers,
    retry: 3,
    retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30000),
  })
 
  if (error) return (
    <div>
      <p>Gagal memuat data</p>
      <button onClick={() => refetch()}>Coba lagi</button>
    </div>
  )
  return <ul>{data?.map((u) => <li key={u.id}>{u.name}</li>)}</ul>
}

retry: 3 retries up to three times, and retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30000) uses exponential backoff capped at 30 seconds — preventing the server from being overloaded when the network is down.

Centralized Error Handling and Fallback UI

Error Boundaries for Rendering, State for Data

Handle two types of errors differently:

  • Render errors (bugs in components) are caught by the error boundaries from episode 7.
  • Data errors (failed requests) are handled per query with state and a fallback UI.
JSInformative fallback UI
function Fallback({ pesan, onRetry }) {
  return (
    <div role="alert">
      <p>Maaf, terjadi kesalahan: {pesan}</p>
      <button onClick={onRetry}>Muat ulang</button>
    </div>
  )
}

<div role="alert">{:javascript}</div> announces the error to screen readers. A good fallback tells users what went wrong and gives them a way out — not just a blank screen.

Centralize: One Way to Call the API

Centralized error handling means all requests pass through a single point that handles failures consistently — for example, an axios interceptor that catches status 401 to trigger automatic logout, or a useApi hook that wraps useQuery with uniform error configuration. One policy change is enough in a single place.

Preloading Data and Optimistic Updates

Preloading: Prepare Before It's Needed

Preloading fetches data early, for example when the user hovers over a button, so the target page feels instant:

JSPrefetch on hover
import { useQueryClient } from "@tanstack/react-query"
 
function Menu() {
  const queryClient = useQueryClient()
 
  const prefetchUsers = () => {
    queryClient.prefetchQuery({ queryKey: ["users"], queryFn: getUsers })
  }
 
  return <Link to="/users" onMouseEnter={prefetchUsers}>Pengguna</Link>
}

queryClient.prefetchQuery({ queryKey: ["users"], queryFn: getUsers }) fetches data in the background when the cursor enters the link. When the page opens, the data is already in the cache — no spinner.

Optimistic Updates: UI Ahead of the Server

For fast actions like likes or deletes, an optimistic update changes the UI first, then updates the server in the background:

JSOptimistic update
const { mutate } = useMutation({
  mutationFn: hapusUser,
  onMutate: async (id) => {
    await queryClient.cancelQueries({ queryKey: ["users"] })
    queryClient.setQueryData(["users"], (old) =>
      old.filter((u) => u.id !== id)
    )
  },
})

onMutate immediately updates the cache before the server response arrives. If the server fails, use onError to restore the cache to its previous state — and show a brief message that synchronization failed.

Install a GraphQL client to practice
npm install urql graphql

The npm install urql graphql command installs a GraphQL client. Try replacing one of your REST fetches with a GraphQL query to feel the difference.

Conclusion

Episode 13 made your API integration robust: an API layer for REST and GraphQL, retries with exponential backoff, centralized error handling with fallback UI, then preloading and optimistic updates for smooth UX.

Key takeaways:

  • Separate API logic into its own module for easy reuse and testing.
  • GraphQL uses queries to fetch only the needed fields.
  • Automatic retries with exponential backoff withstand temporary network issues.
  • Error boundaries handle render errors; state handles data errors.
  • Fallback UI must provide a message and a way out.
  • Prefetch and optimistic updates make the UI feel responsive.

In the next episode, episode 14, we'll cover networking performance — HTTP caching and stale-while-revalidate, code splitting and bundle analysis, prefetching data and assets, and monitoring network performance. Your app won't just work — it'll be fast.

Learn ReactJS - API Integration & Error Handling | Learn ReactJS