ViteJs Type-Safe Web Worker in React

Languages

typescript5.7.2

Libraries

react18.3.1
GithubCode

Web Workers allow offloading expensive computations on a background thread in browsers.

vite supports creating Web Workers with new Worker.

const url = new URL("./feed.ts", import.meta.url); // 👈 Add `import.meta.url` for Vite
const newWorker = new Worker(url, { type: "module" });

Since Web Workers are processes outside of React, this is a valid case of using useEffect to create and interact with a worker.

Make sure to call terminate() in useEffect clean up function.

Interacting with a worker consists of two operations:

  • Sending messages (postMessage)
  • Receiving messages (onmessage)

onmessage can be made type-safe by passing the expected messages inside MessageEvent (e.g. MessageEvent<WorkerResponse>).

postMessage accepts any as parameter. We can make it type-safe as well by using satisfies to enforce the value provided:

worker.current.postMessage({
  type: "feed.get",
  source: "https://www.sandromaglione.com/feed",
} satisfies WorkerMessage);

The worker script implements onmessage as well. Inside it you can perform any (expensive) computation, and then send back a message with the result to the main thread (postMessage).

Inside vite.config.ts you can specify the output format of the worker bundle (e.g. "es"):

vite.config.ts
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [react()],
  worker: { format: "es" },
});

Make sure to also specify type: "module" when creating the Worker instance (otherwise you will get the following error: SyntaxError: Cannot use import statement outside a module):

const url = new URL("./feed.ts", import.meta.url);
const newWorker = new Worker(url,
  { type: "module" }
);
import { useEffect, useRef } from "react";
import type { WorkerMessage, WorkerResponse } from "./types";

export default function App() {
  const worker = useRef<Worker | null>(null);

  // Send message to worker (type-safe from `WorkerMessage`)
  const sendMessage = () => {
    if (worker.current) {
      worker.current.postMessage({
        type: "feed.get",
        source: "https://www.sandromaglione.com/feed",
      } satisfies WorkerMessage);
    }
  };

  useEffect(() => {
    const url = new URL("./feed.ts", import.meta.url);
    const newWorker = new Worker(url, { type: "module" });

    // Handle all possible messages (type-safe from `WorkerResponse`)
    newWorker.onmessage = (event: MessageEvent<WorkerResponse>) => {
      if (event.data.type === "feed.get") {
        // Received message from worker (type-safe) ✨
      }
    };

    newWorker.onerror = (error: ErrorEvent) => {
      // Handle error from worker (`ErrorEvent`)
    };

    worker.current = newWorker;

    return () => {
      // Clean up worker when component unmounts
      newWorker.terminate();
    };
  }, []);

  return (
    <button type="button" onClick={sendMessage}>
      Send
    </button>
  );
}