useWorker
Run heavy JavaScript functions in a background Web Worker to prevent UI freezing. The perfect companion to useWasm.
Installation
npx shadcn@latest add https://r.fiberui.com/r/hooks/use-worker.jsonA React hook for safely running heavy JavaScript functions off the main thread using Web Workers. Perfect for complex data parsing, huge array manipulations, or heavy math calculations that would otherwise freeze your UI.
Worker Scope limitations
The function passed to useWorker runs in an isolated environment. It
cannot access DOM elements, window objects, or any external scope
variables that aren't passed explicitly through arguments.
Features
- Non-blocking - Keeps the main UI thread responsive by running functions in the background
- Inline Worker - Translates your function into an inline Blob. No extra files required!
- State Management - Returns handy
loading,result, anderrorstates - Type Safe - Fully generics-based typing for arguments and return types
Basic Usage
Heavy Math Processing
Heavy Math Processing
Calculating recursive Fibonacci without freezing the UI.
Try calculating Fibonacci(40). If this ran on the main thread, the UI would freeze completely until it finished.
"use client";
import { useWorker } from "@repo/hooks/performance/use-worker";
import { useState } from "react";
import { Button } from "@repo/ui/components/button";
import { Card } from "@repo/ui/components/card";
import { Loader2, Calculator } from "lucide-react";
// This function will seamlessly run in a background thread
// It must not use external variables from the React component
function calculateFibonacci(n: number): number {
if (n <= 1) return n;
return calculateFibonacci(n - 1) + calculateFibonacci(n - 2);
}
export function Example1() {
const { execute, result, loading, error } = useWorker(calculateFibonacci);
const [num, setNum] = useState(40); // 40 is high enough to freeze main thread normally
return (
<Card className="mx-auto flex max-w-sm flex-col gap-6 p-6">
<div className="flex items-center gap-3 border-b pb-4">
<div className="bg-primary/10 text-primary rounded-lg p-2">
<Calculator className="h-5 w-5" />
</div>
<div>
<h3 className="font-bold">Heavy Math Processing</h3>
<p className="text-muted-foreground mt-0.5 text-xs">
Calculating recursive Fibonacci without freezing the UI.
</p>
</div>
</div>
<div className="flex flex-col gap-3 text-sm">
<p>
Try calculating Fibonacci({num}). If this ran on the main
thread, the UI would freeze completely until it finished.
</p>
<div className="flex gap-2">
<Button
variant="outline"
onClick={() => setNum((n) => Math.max(1, n - 1))}
isDisabled={loading}
>
-
</Button>
<div className="bg-muted/50 flex flex-1 items-center justify-center rounded-md border font-mono font-bold">
{num}
</div>
<Button
variant="outline"
onClick={() => setNum((n) => n + 1)}
isDisabled={loading}
>
+
</Button>
</div>
<Button
onClick={() => execute(num)}
isDisabled={loading}
className="mt-2 w-full"
>
{loading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Calculating...
</>
) : (
"Calculate off-thread"
)}
</Button>
</div>
{error && (
<div className="rounded-md border border-red-500/20 bg-red-500/10 p-3 text-sm text-red-500">
{error.message}
</div>
)}
{result !== undefined && !loading && (
<div className="flex flex-col items-center justify-center rounded-lg border border-green-500/20 bg-green-500/10 p-4 text-green-700 dark:text-green-400">
<span className="mb-1 text-xs font-bold uppercase tracking-wider">
Result
</span>
<span className="font-mono text-2xl font-black tracking-tighter">
{result.toLocaleString()}
</span>
</div>
)}
</Card>
);
}
Massive Array Sorting
Massive Array Sorting
Generating and sorting datasets asynchronously.
"use client";
import { useWorker } from "@repo/hooks/performance/use-worker";
import { useState } from "react";
import { Button } from "@repo/ui/components/button";
import { Card } from "@repo/ui/components/card";
import { Database, Loader2, ArrowRight } from "lucide-react";
// Self-contained massive array sorter using a fake dataset
const sortMassiveArray = (
size: number,
): { count: number; timeTaken: number; sample: any[] } => {
const start = performance.now();
// Generate massive array
const data = Array.from({ length: size }).map((_, i) => ({
id: i,
name: `User ${Math.random().toString(36).substring(7)}`,
score: Math.floor(Math.random() * 100000),
}));
// Perform an expensive sort operation
data.sort((a, b) => b.score - a.score);
const timeTaken = Math.round(performance.now() - start);
// Return metadata and top 5 scores
return {
count: data.length,
timeTaken,
sample: data.slice(0, 5),
};
};
export function Example2() {
const { execute, result, loading } = useWorker(sortMassiveArray);
const [arraySize, setArraySize] = useState(500000);
return (
<Card className="mx-auto flex max-w-md flex-col gap-6 p-6">
<div className="flex items-center gap-3 border-b pb-4">
<div className="rounded-lg bg-blue-500/10 p-2 text-blue-500">
<Database className="h-5 w-5" />
</div>
<div>
<h3 className="font-bold">Massive Array Sorting</h3>
<p className="text-muted-foreground mt-0.5 text-xs">
Generating and sorting datasets asynchronously.
</p>
</div>
</div>
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground font-medium">
Records to Sort:
</span>
<span className="bg-muted rounded px-2 py-1 font-bold tabular-nums">
{arraySize.toLocaleString()}
</span>
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setArraySize(100000)}
className="flex-1"
>
100K
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setArraySize(500000)}
className="flex-1"
>
500K
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setArraySize(1000000)}
className="flex-1"
>
1M
</Button>
</div>
<Button
onClick={() => execute(arraySize)}
isDisabled={loading}
className="mt-2 bg-blue-600 text-white hover:bg-blue-700"
>
{loading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />{" "}
Sorting Data In Background...
</>
) : (
"Start Worker Process"
)}
</Button>
{/* Keep UI interactive during processing */}
<div className="mt-2 rounded border bg-zinc-100 p-2 text-center text-xs dark:bg-zinc-900">
Try typing here while sorting:{" "}
<input
type="text"
className="bg-background ml-2 w-24 border px-1 outline-none"
placeholder="Responsive!"
/>
</div>
</div>
{result && !loading && (
<div className="animate-in slide-in-from-bottom-2 fade-in mt-2 flex flex-col gap-2">
<div className="text-muted-foreground mb-1 flex items-center justify-between text-xs font-bold uppercase tracking-wider">
<span>Top 5 Results</span>
<span className="bg-primary text-primary-foreground rounded-full px-2 py-0.5 lowercase">
{result.timeTaken}ms
</span>
</div>
<div className="flex flex-col gap-1 rounded-lg bg-zinc-950 p-2 font-mono text-sm text-zinc-300">
{result.sample.map((s, i) => (
<div
key={s.id}
className="flex justify-between border-b border-zinc-800 px-2 py-1 last:border-0"
>
<span>
{i + 1}. {s.name}
</span>
<span className="text-green-400">
{s.score}
</span>
</div>
))}
</div>
</div>
)}
</Card>
);
}
Heavy JSON Parsing
Heavy JSON Parsing
Parse and transform large datasets in the background.
Ready to process 50MB of raw logs
"use client";
import { useWorker } from "@repo/hooks/performance/use-worker";
import { useState } from "react";
import { Button } from "@repo/ui/components/button";
import { Card } from "@repo/ui/components/card";
import { FileDown, FileJson, Loader2, Link } from "lucide-react";
// Mock raw API response payload
const rawDataString = `
[
{"id": "a1", "timestamp": "2024-03-12T10:00:00Z", "type": "auth", "status": 200, "meta": {"ip": "192.168.1.1", "agent": "Mozilla"}},
{"id": "b2", "timestamp": "2024-03-12T10:01:15Z", "type": "query", "status": 500, "meta": {"queryTime": "45ms"}},
{"id": "c3", "timestamp": "2024-03-12T10:05:30Z", "type": "auth", "status": 401, "meta": {"ip": "10.0.0.5"}}
]
`;
// Simulate fetching and parsing a huge JSON string
const parseAndTransformData = (
jsonString: string,
): { typeCounts: Record<string, number>; errors: number; items: any[] } => {
// Artificial delay to simulate huge file processing
const start = Date.now();
while (Date.now() - start < 1500) {}
// Parse the data
const data = JSON.parse(jsonString);
// Transform and aggregate the data
const typeCounts: Record<string, number> = {};
let errors = 0;
const transformed = data.map((item: any) => {
// Aggregate types
typeCounts[item.type] = (typeCounts[item.type] || 0) + 1;
// Count errors
if (item.status >= 400) errors++;
// Return flattened format
return {
id: item.id,
time: new Date(item.timestamp).toLocaleTimeString(),
type: item.type.toUpperCase(),
error: item.status >= 400,
};
});
return { typeCounts, errors, items: transformed };
};
export function Example3() {
const { execute, result, loading, error } = useWorker(
parseAndTransformData,
);
const [viewRaw, setViewRaw] = useState(false);
return (
<Card className="mx-auto flex w-full max-w-lg flex-col gap-6 bg-zinc-50 p-6 dark:bg-zinc-900/40">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="rounded-lg bg-purple-500/10 p-2 text-purple-600">
<FileJson className="h-5 w-5" />
</div>
<div>
<h3 className="font-bold">Heavy JSON Parsing</h3>
<p className="text-muted-foreground mt-0.5 text-xs">
Parse and transform large datasets in the
background.
</p>
</div>
</div>
<Button
variant="outline"
size="icon"
onClick={() => setViewRaw(!viewRaw)}
>
{viewRaw ? (
<Link className="h-4 w-4" />
) : (
<FileDown className="h-4 w-4" />
)}
</Button>
</div>
{viewRaw ? (
<div className="max-h-48 overflow-auto rounded-md border border-zinc-800 bg-zinc-950 p-4 font-mono text-xs text-emerald-400">
<pre>{rawDataString.trim()}</pre>
</div>
) : (
<div className="flex flex-col gap-4">
{!result ? (
<div className="text-muted-foreground bg-background flex flex-col items-center justify-center rounded-xl border border-dashed py-8 text-center">
<p className="mb-4 text-sm font-medium">
Ready to process 50MB of raw logs
</p>
<Button
onClick={() => execute(rawDataString)}
isDisabled={loading}
className="bg-purple-600 text-white hover:bg-purple-700"
>
{loading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />{" "}
Decoding & Parsing...
</>
) : (
"Start worker job"
)}
</Button>
</div>
) : (
<div className="animate-in fade-in slide-in-from-bottom-2 flex flex-col gap-4">
<div className="grid grid-cols-2 gap-3">
<div className="bg-background flex flex-col items-center justify-center rounded-xl border p-4 text-center shadow-sm">
<span className="text-3xl font-black text-purple-600 dark:text-purple-400">
{result.items.length}
</span>
<span className="text-muted-foreground mt-1 text-xs font-bold uppercase tracking-wider">
Rows Parsed
</span>
</div>
<div className="bg-background flex flex-col items-center justify-center rounded-xl border p-4 text-center shadow-sm">
<span className="text-3xl font-black text-red-500">
{result.errors}
</span>
<span className="text-muted-foreground mt-1 text-xs font-bold uppercase tracking-wider">
Errors Found
</span>
</div>
</div>
<div className="bg-background overflow-hidden rounded-xl border text-sm">
<div className="bg-muted border-b px-4 py-2 text-xs font-semibold">
Processed Logs Preview
</div>
<div className="max-h-32 divide-y overflow-auto">
{result.items.map((item: any) => (
<div
key={item.id}
className="hover:bg-muted/50 flex items-center justify-between px-4 py-2 transition-colors"
>
<div className="flex items-center gap-3">
<span className="font-mono text-xs">
{item.id}
</span>
<span
className={`rounded px-2 py-0.5 text-[10px] font-bold ${item.type === "AUTH" ? "bg-blue-100 text-blue-700 dark:bg-blue-900/30" : "bg-orange-100 text-orange-700 dark:bg-orange-900/30"}`}
>
{item.type}
</span>
</div>
<span
className={`font-mono text-xs ${item.error ? "font-bold text-red-500" : "text-muted-foreground"}`}
>
{item.error ? "FAILED" : "OK"}
</span>
</div>
))}
</div>
</div>
<Button
variant="outline"
onClick={() => execute(rawDataString)}
isDisabled={loading}
className="w-full"
>
{loading ? "Re-processing..." : "Process Again"}
</Button>
</div>
)}
</div>
)}
{error && (
<div className="rounded-lg border border-red-500/20 bg-red-500/10 p-3 text-sm font-medium text-red-500">
{error.message}
</div>
)}
</Card>
);
}
Error Handling Bounds
Error Handling Bounds
If a Web Worker crashes or throws an exception, it is caught safely by the hook without crashing the main React tree.
"use client";
import { useWorker } from "@repo/hooks/performance/use-worker";
import { Button } from "@repo/ui/components/button";
import { Card } from "@repo/ui/components/card";
import { AlertCircle, TerminalSquare } from "lucide-react";
// Functions in workers must throw standard Javascript Errors
const faultyWorkerFunction = (shouldCrash: boolean) => {
// Artificial delay to mimic processing
const start = Date.now();
while (Date.now() - start < 800) {}
if (shouldCrash) {
throw new Error("Out of memory exception during AST traversal module.");
}
return "Process completed perfectly.";
};
export function Example4() {
const { execute, result, loading, error } = useWorker(faultyWorkerFunction);
return (
<Card className="mx-auto flex max-w-md flex-col gap-6 border-red-500/20 bg-red-500/5 p-6">
<div className="flex items-start gap-4">
<div className="mt-1 rounded-xl bg-red-500/20 p-3 text-red-600 dark:text-red-400">
<AlertCircle className="h-6 w-6" />
</div>
<div>
<h3 className="text-lg font-bold text-red-600 dark:text-red-300">
Error Handling Bounds
</h3>
<p className="text-foreground/80 mt-1 text-sm">
If a Web Worker crashes or throws an exception, it is
caught safely by the hook without crashing the main
React tree.
</p>
</div>
</div>
<div className="flex gap-3">
<Button
onClick={() => execute(false)}
isDisabled={loading}
variant="outline"
className="flex-1"
>
Run Safe Task
</Button>
<Button
onClick={() => execute(true)}
isDisabled={loading}
variant="destructive"
className="flex-1"
>
Force Crash Script
</Button>
</div>
<div className="bg-background mt-2 overflow-hidden rounded-md border">
<div className="bg-muted/40 flex items-center justify-between border-b px-3 py-2">
<span className="text-muted-foreground text-xs font-semibold uppercase">
Console Output
</span>
<TerminalSquare className="text-muted-foreground h-3 w-3" />
</div>
<div className="flex min-h-[100px] flex-col justify-center bg-zinc-950 p-4 font-mono text-xs text-zinc-300">
{loading ? (
<span className="animate-pulse text-blue-400">
Running worker thread...
</span>
) : error ? (
<div className="wrap-break-word flex flex-col gap-1 text-red-400">
<span className="font-bold">
> Exception Caught in thread:
</span>
<span>{error.message}</span>
</div>
) : result ? (
<span className="text-green-400">> {result}</span>
) : (
<span className="text-zinc-600">
> Waiting for execution...
</span>
)}
</div>
</div>
</Card>
);
}
API Reference
Hook Signature
function useWorker<TArgs extends any[], TReturn>(
workerFunction: (...args: TArgs) => TReturn,
): UseWorkerReturn<TArgs, TReturn>;Parameters
| Parameter | Type | Description |
|---|---|---|
workerFunction | (...args: TArgs) => TReturn | A completely self-contained function to execute. |
Return Value
| Property | Type | Description |
|---|---|---|
execute | (...args: TArgs) => void | Invokes the worker function with the provided args |
result | TReturn | undefined | The output returned by the worker function |
loading | boolean | true while the worker is busy |
error | Error | undefined | Detailed error object if the worker throws an error |
Hook Source Code
import { useState, useEffect, useRef, useCallback } from "react";
/**
* Run heavy JavaScript functions in a background Web Worker.
*
* @param workerFunction The function that will be executed in the worker. Should be self-contained.
*/
export function useWorker<TArgs extends any[], TReturn>(
workerFunction: (...args: TArgs) => TReturn,
) {
const [result, setResult] = useState<TReturn | undefined>(undefined);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<Error | undefined>(undefined);
const workerRef = useRef<Worker | null>(null);
// Initialize worker
useEffect(() => {
const code = `
self.onmessage = async function(e) {
try {
const fn = ${workerFunction.toString()};
const result = await fn(...e.data);
self.postMessage({ status: 'SUCCESS', result });
} catch (error) {
self.postMessage({ status: 'ERROR', error: error.message });
}
}
`;
const blob = new Blob([code], { type: "application/javascript" });
const url = URL.createObjectURL(blob);
const worker = new Worker(url);
workerRef.current = worker;
worker.onmessage = (e) => {
if (e.data.status === "SUCCESS") {
setResult(e.data.result);
setError(undefined);
} else {
setError(new Error(e.data.error));
}
setLoading(false);
};
worker.onerror = (e) => {
setError(new Error(e.message));
setLoading(false);
};
return () => {
worker.terminate();
URL.revokeObjectURL(url);
};
}, [workerFunction]);
const execute = useCallback((...args: TArgs) => {
if (!workerRef.current) return;
setLoading(true);
setError(undefined);
workerRef.current.postMessage(args);
}, []);
return { execute, loading, result, error };
}