Skip to main content

Working with asynchronous operations

1. React itself is a synchronous library

React does not know when or how you load data, call an API, or run promises. Its job is to render the interface based on state (state and props). Asynchrony appears when you change state as a result of an external operation - and then React re-renders.


2. The typical way - through useEffect()

Asynchronous operations in function components are usually run inside useEffect, because:

  • the effect runs after the render,
  • you cannot await directly in the component's body (a render must be a pure function).

Example:

javascript
import { useState, useEffect } from "react"; function UserProfile({ userId }) { const [user, setUser] = useState(null); useEffect(() => { let isMounted = true; async function fetchUser() { const res = await fetch(`/api/users/${userId}`); const data = await res.json(); if (isMounted) setUser(data); } fetchUser(); // cleanup - if the component unmounts before the request finishes return () => { isMounted = false; }; }, [userId]); if (!user) return <p>Loading...</p>; return <p>Hello, {user.name}</p>; }

Important: useEffect cannot be made async directly - instead, you create a nested async function.


3. Asynchronous operations = side effects

Classic examples:

  • loading data from an API (fetch, axios, GraphQL);
  • interacting with WebSocket or SSE;
  • timers (setTimeout, setInterval);
  • operations with local storage (localStorage, IndexedDB);
  • working with external SDKs (for example, Firebase).

All of them run outside React, but affect state.


4. Modern approaches

React Query (TanStack Query)

One of the most popular ways to work with asynchronous data is TanStack Query. It:

  • caches results;
  • automatically refetches on window focus;
  • supports pagination, invalidation, optimistic updates.

Example:

javascript
import { useQuery } from "@tanstack/react-query"; function UserProfile({ userId }) { const { data, isLoading } = useQuery({ queryKey: ["user", userId], queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json()), }); if (isLoading) return <p>Loading...</p>; return <p>Hello, {data.name}</p>; }

5. Asynchrony in React 18+ (Concurrent features)

React 18 introduced new capabilities for asynchronous rendering:

  • startTransition() lets you mark updates as "non-urgent" so they do not block the UI.
  • useTransition() and useDeferredValue() let you defer a render or update the UI smoothly.
  • Suspense + React.lazy() are for lazily loading components and waiting for data.

Example with Suspense:

javascript
const UserProfile = React.lazy(() => import("./UserProfile")); function App() { return ( <Suspense fallback={<p>Loading...</p>}> <UserProfile /> </Suspense> ); }

In React Server Components (RSC), asynchrony is built in - server components can be async and use await right in their body.


6. The key idea

Asynchrony in React is not the same as asynchrony of React itself. React stays deterministic and synchronous at the render level, and asynchrony is a way of changing state from the outside.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.