Rohit Nandi

Full Stack Engineer

React Concurrency
React Concurrency (Published on 8th Nov, 2025)

TLDR

JavaScript is single threaded, but browsers provide multiple background threads for network requests, timers, and rendering. React 18's concurrent rendering adds a smart scheduler that can pause and resume rendering work, keeping your UI responsive even during heavy updates. Multiple API calls run truly in parallel on network threads, while React's scheduler ensures user interactions don't get blocked by expensive renders.

The Confusion: Single Thread But Multiple Things Happening

You've probably heard "JavaScript is single threaded" countless times. But then you see your React app doing multiple things at once: API calls loading, spinners animating, users scrolling and clicking. How is this possible?

The answer lies in understanding what's actually single threaded and what isn't.

JavaScript vs The Browser

JavaScript itself runs on one main thread. Only one piece of JS code executes at any moment. But the browser is multi threaded and provides:

  • Network threads: Handle fetch/XHR requests in the background
  • Rendering threads: Paint pixels and process user input
  • Web API thread pool: Manage timers, I/O operations, etc.

So while JS can't literally run two functions simultaneously, it can start async tasks and continue doing other work while they complete in the background.

The Event Loop: How It All Works Together

Here's the simplified flow:

  1. JavaScript executes a task and schedules async operations (API calls, timers, etc.)
  2. Browser handles them on other threads
  3. When done, results go into the event queue
  4. The event loop picks the next callback when the main thread is free

This is asynchronous interleaving, not true parallelism. But it feels concurrent to users.

Example: Multiple API Calls + DOM Updates

Let's see this in action:

function Dashboard() {
  const [data, setData] = useState(null)

  useEffect(() => {
    // These two API calls start simultaneously on network threads
    const p1 = fetch('/api/users').then((r) => r.json())
    const p2 = fetch('/api/stats').then((r) => r.json())

    Promise.all([p1, p2]).then(([users, stats]) => {
      // Update DOM with both results
      setData({ users, stats })
    })
  }, [])

  // While APIs are loading, users can still click, scroll, type
  return <div>{data ? <DashboardUI {...data} /> : <p>Loading...</p>}</div>
}

What happens here:

  1. Both fetch calls start simultaneously on network threads
  2. The main thread is free to handle user input or render the loading spinner
  3. When both fetches finish, results get queued as microtasks
  4. React processes the state update and re-renders the component

From the user's perspective: APIs are loading, DOM is updating, and they can interact with the page. It feels concurrent.

React 18's Concurrent Rendering

React 18 takes this further with concurrent rendering. It's not multithreading, but a smarter scheduler that can interrupt rendering for urgent work.

Before React 18: Rendering was synchronous. One long render could freeze the UI.

With React 18: Rendering is interruptible. React can pause low priority updates to handle high priority ones like typing or button clicks.

Here's a practical example:

import { startTransition } from 'react'

function Search() {
  const [query, setQuery] = useState('')
  const [results, setResults] = useState([])

  function handleInput(e) {
    const value = e.target.value

    // This update is urgent - keep the input responsive
    setQuery(value)

    // Mark this as non-urgent
    startTransition(() => {
      fetch(`/api/search?q=${value}`)
        .then((r) => r.json())
        .then(setResults)
    })
  }

  return (
    <div>
      <input value={query} onChange={handleInput} />
      <ResultsList results={results} />
    </div>
  )
}

What's happening:

  • setQuery(value) updates immediately (urgent)
  • startTransition tells React: "This update can wait if the user is typing"
  • If the user types fast, React can pause rendering partial results
  • The input stays smooth and responsive

Combining Multiple APIs with Concurrent Rendering

You can combine both concepts for a fully responsive experience:

function MultiDataView() {
  const [users, setUsers] = useState([])
  const [stats, setStats] = useState(null)

  useEffect(() => {
    // Fire both APIs at once on network threads
    const p1 = fetch('/api/users').then((r) => r.json())
    const p2 = fetch('/api/stats').then((r) => r.json())

    Promise.all([p1, p2]).then(([usersData, statsData]) => {
      // Mark as low priority so user interactions stay smooth
      startTransition(() => {
        setUsers(usersData)
        setStats(statsData)
      })
    })
  }, [])

  return (
    <>
      {!users.length ? <Spinner /> : <UsersTable data={users} />}
      {!stats ? <Spinner /> : <StatsPanel data={stats} />}
    </>
  )
}

Both requests run concurrently on network threads, React schedules updates efficiently, and user interactions remain smooth throughout.

When You Need True Parallelism: Web Workers

If your JavaScript itself is CPU heavy (large JSON parsing, cryptography, image processing), even React's concurrency won't help. It's still one thread doing the work.

For these cases, use Web Workers:

// main.js
const worker = new Worker('worker.js')
worker.postMessage(largeData)
worker.onmessage = (e) => {
  console.log('Result:', e.data)
}

// worker.js
onmessage = (e) => {
  const result = heavyComputation(e.data)
  postMessage(result)
}

Web Workers run truly in parallel on a different thread.

Quick Reference: What's Concurrent and What's Not

Task TypeIs True ConcurrencyHandled ByExample
Multiple API callsYes (network threads)BrowserPromise.all([fetch1, fetch2])
DOM updatesNo (same thread)React schedulerConcurrent rendering
User input during renderingYes (interruptible)React 18 schedulerstartTransition()
CPU heavy JSYes (if offloaded)Web Workernew Worker()

Conclusion

React's concurrency isn't about running JavaScript in parallel. It's about:

  1. Browser concurrency: Network threads handle API calls simultaneously
  2. Smart scheduling: React 18 can interrupt low priority work for urgent updates
  3. Better UX: Users get responsive interfaces even during heavy operations

Understanding this helps you write better React apps. Use Promise.all() for parallel API calls, startTransition() for non-urgent updates, and Web Workers for CPU intensive tasks.

The key insight: JavaScript is single threaded, but the browser isn't. React 18 adds a scheduler that makes the best use of that single thread, keeping your UI responsive and your users happy.

© 2025 Rohit