Multi-Threaded React Apps with useWorker: A Simple Guide

ReactJavaScriptWeb DevelopmentPerformance OptimizationWeb WorkersFrontend

October 14, 2025

Banner

What This Article Covers

JavaScript runs on a single thread, meaning it can only do one thing at a time. If your app has a heavy task—like sorting a huge list of numbers—it can freeze the user interface (UI), making the app feel slow and unresponsive. This is bad for user experience (UX). To fix this, we can use Web Workers, which let you run heavy tasks in the background on a separate thread, so the main UI stays smooth.

In this guide, we'll explain Web Workers in simple terms, compare them to React's Concurrent Mode, and introduce the useWorker library. This library makes it easy to add multi-threading to your React apps using hooks. We'll walk through an example of sorting a large array, show code snippets, and discuss when to use it, plus its limitations.

I'll paraphrase the original blog in easier language and add extra explanations where things might be confusing. For instance, I'll explain terms like "thread" (think of it as a worker bee handling one job) and why blocking happens (the main thread gets stuck on the heavy task, ignoring user clicks or animations).

Understanding Web Workers

A Web Worker is like a helper script that runs separately from your main JavaScript code. It doesn't touch the UI, so it won't freeze your app. Normally, everything in a browser (like button clicks, animations, and calculations) happens on the "main thread." If a task takes too long, the main thread is busy, and the UI lags.

intro-image

Extra Explanation: Imagine the main thread as a single chef in a kitchen. If the chef is busy chopping a mountain of veggies (heavy task), they can't serve customers (handle UI interactions). A Web Worker is like hiring a second chef in a separate room—they handle the chopping without stopping the service.

Web Workers are great for:

  • Sorting big datasets.
  • Exporting large CSV files.
  • Editing images.
  • Any CPU-heavy job that might slow down the browser.

Without a worker, a heavy task blocks the UI. With one, it runs in parallel, keeping things responsive.

How Does This Compare to React's Concurrent Mode?

React's Concurrent Mode (using features like startTransition) doesn't truly run tasks in parallel. It just prioritizes urgent tasks (like user input) over non-urgent ones (like rendering a big table). Everything still happens on the main thread, but React switches between tasks quickly.

On Concurrent Mode concurrent-mode-image Using React useWorkers use-workers-image

Extra Explanation: This is like time-sharing on one computer—pausing a slow download to check email. It's helpful but not as powerful as true multi-threading, where tasks run simultaneously on different "cores" (like multiple computers working together).

In contrast, Web Workers use actual separate threads, so a big table render won't interrupt a search bar at all.

Introducing useWorker: A Simpler Way to Use Web Workers

The useWorker library is a small (just 3KB) React hook that wraps Web Worker APIs. It makes setup easy—no messy event listeners or complex code. Key features include:

  • Support for promises (easier than old-school events).
  • Timeout to stop a worker if it takes too long.
  • Handling remote dependencies.
  • Transferable data (efficiently passing large objects).
  • Worker status tracking (e.g., running, success).

Why use this instead of plain JavaScript Web Workers? Plain ones require more boilerplate code, like creating a separate worker file and handling messages manually. useWorker simplifies it to a single hook.

Code Comparison

Here's a quick side-by-side to show the difference. We're sorting an array of numbers.

Plain JavaScript Web Worker: You need a separate file for the worker:

// webworker.js
self.addEventListener("message", function(event) {
  var numbers = [...event.data];
  postMessage(numbers.sort());
});

Then in your main React file:

// index.js
var webworker = new Worker("./webworker.js");
webworker.postMessage([3, 2, 1]);
webworker.addEventListener("message", function(event) {
  console.log("Message from worker:", event.data); // [1, 2, 3]
});

With useWorker: No separate file needed—just a function:

// index.js
const sortNumbers = numbers => ([...numbers].sort());
const [sortWorker] = useWorker(sortNumbers);
const result = await sortWorker([1, 2, 3]);

See? Much cleaner. useWorker handles the worker creation and communication behind the scenes.

Getting Started: Example with Sorting a Large Array

Let's build a simple React app that sorts 50,000 random numbers. We'll compare doing it on the main thread (which freezes the UI) vs. using useWorker (which doesn't).

Step 1: Install the Library

Run this in your terminal:

yarn add @koale/useworker

Or use npm if you prefer:

npm install @koale/useworker

Step 2: Import and Set Up

In your component:

import { useWorker, WORKER_STATUS } from "@koale/useworker";
  • useWorker returns a worker function and a controller (with status and a kill method).
  • WORKER_STATUS helps track if the worker is running, succeeded, etc.

Extra Explanation: The "kill" function is like hitting stop on a long-running process—useful if the user cancels or it times out.

Step 3: Create the Sorting Component

Here's the full code for a SortingArray.js component. It uses a bubble sort algorithm (simple but slow for big lists, perfect for demoing slowness). We also add toast notifications for feedback.

First, define a bubble sort function in a separate file like algorithms/bubblesort.js:

// bubblesort.js
export default function bubleSort(numbers) {
  // Simple bubble sort implementation - swaps adjacent elements if out of order
  for (let i = 0; i < numbers.length; i++) {
    for (let j = 0; j < numbers.length - i - 1; j++) {
      if (numbers[j] > numbers[j + 1]) {
        [numbers[j], numbers[j + 1]] = [numbers[j + 1], numbers[j]];
      }
    }
  }
  return numbers;
}

Now the component:

import React from "react";
import { useWorker, WORKER_STATUS } from "@koale/useworker";
import { useToasts } from "react-toast-notifications";
import bubleSort from "./algorithms/bublesort";

const numbers = [...Array(50000)].map(() => Math.floor(Math.random() * 1000000));

function SortingArray() {
  const { addToast } = useToasts();
  const [sortStatus, setSortStatus] = React.useState(false);
  const [sortWorker, { status: sortWorkerStatus }] = useWorker(bubleSort);

  const onSortClick = () => {
    setSortStatus(true);
    const result = bubleSort(numbers); // Runs on main thread
    setSortStatus(false);
    addToast("Finished: Sort", { appearance: "success" });
    console.log("Bubble Sort", result);
  };

  const onWorkerSortClick = async () => {
    const result = await sortWorker(numbers); // Runs in worker
    console.log("Bubble Sort useWorker()", result);
    addToast("Finished: Sort using useWorker.", { appearance: "success" });
  };

  return (
    <div>
      <section>
        <button disabled={sortStatus} onClick={onSortClick}>
          {sortStatus ? "Loading..." : "Bubble Sort (Main Thread)"}
        </button>
        <button disabled={sortWorkerStatus === WORKER_STATUS.RUNNING} onClick={onWorkerSortClick}>
          {sortWorkerStatus === WORKER_STATUS.RUNNING ? "Loading..." : "Bubble Sort useWorker()"}
        </button>
      </section>
      <section>
        <span>Open console to see results.</span>
      </section>
    </div>
  );
}

export default SortingArray;

Step 4: Add to Your Main App

In App.js, add a rotating logo to visualize UI blocking (it stops rotating if the main thread is busy). Install react-toast-notifications for toasts.

import React from "react";
import { ToastProvider } from "react-toast-notifications";
import SortingArray from "./SortingArray";
import logo from "./react.png"; // Your React logo image
import "./style.css"; // Basic styles for rotation

let turn = 0;

function infiniteLoop() {
  const logoElement = document.querySelector(".App-logo");
  turn += 8;
  logoElement.style.transform = `rotate(${turn % 360}deg)`;
}

export default function App() {
  React.useEffect(() => {
    const loopInterval = setInterval(infiniteLoop, 100);
    return () => clearInterval(loopInterval);
  }, []);

  return (
    <ToastProvider>
      <div className="App">
        <h1>useWorker Demo</h1>
        <header>
          <img src={logo} className="App-logo" alt="logo" />
        </header>
        <hr />
        <SortingArray />
      </div>
    </ToastProvider>
  );
}

How It Works in Action

  • Main Thread Sort: Click the first button—the logo stops rotating (UI blocked) until sorting finishes. This shows how heavy tasks freeze everything. main-thread Let’s check the performance profiling of this using the chrome performance recording. main-thread-perfomance
  • useWorker Sort: Click the second button—the logo keeps spinning! The sort runs in the background. useWorker-sort Let’s check the performance profiling of this using the chrome performance recording. main-thread-perfomance

Extra Explanation: Use browser dev tools (like Chrome's Performance tab) to record. Main thread sort shows a long block (e.g., 3-4 seconds). Worker sort keeps the main thread free, with the task in a separate worker thread.

When Should You Use Web Workers?

Use them for tasks that eat up CPU and might freeze the UI:

  • Processing images (e.g., filters or resizing).
  • Handling huge datasets (sorting, filtering).
  • Exporting big files like CSV or Excel.
  • Drawing on canvas elements.
  • Any intensive calculations.

Don't use them for everything—only when needed, to avoid extra complexity.

Limitations of useWorker and Web Workers

  • No access to browser stuff like window or document (workers are isolated).
  • Can't run the same worker again until it's done (create multiple hooks if needed).
  • Can't return functions from workers (data is serialized, like turning objects into strings).
  • Limited by the user's device (CPU cores, memory)—not magic for super-slow machines.

Extra Explanation: Isolation is for security and performance, but it means you can't directly manipulate the page from a worker. You pass data back and forth via messages.

Wrapping Up

useWorker makes it simple to add multi-threading to React apps, keeping your UI responsive during heavy work. It's built on Web Workers but easier to use with hooks. Remember, don't overuse workers—save them for tasks that truly need them to keep your code clean.

If you're new to this, experiment with the example code. It can make your apps feel much faster!