The Three-Hook Pattern That Keeps React UIs Responsive During Heavy Computation
Any React app that does client-side data processing, image manipulation, or cryptographic work will freeze the UI without this pattern. The three-hook coordination is the minimal correct recipe for Web Workers in React, and getting the cleanup wrong leaks threads that keep consuming memory until the tab closes.
JavaScript's single-threaded runtime freezes the entire page during a long-running loop. Web Workers run scripts on a separate OS thread, but managing their lifecycle inside React requires coordinating three hooks. useRef holds a persistent reference to the Worker instance across re-renders without triggering updates. useEffect creates the Worker on mount and calls terminate() on unmount to prevent memory leaks. useState drives the UI, disabling a button during computation and displaying the result when the Worker posts back. The demo runs a half-million-iteration calculation in the background while the page remains scrollable and responsive.
The communication model is pure message-passing: the main thread calls postMessage, the Worker's onmessage handler runs the loop, and the Worker posts the result back. Workers have no access to the DOM, window, or document, so all UI updates happen in the main thread's onmessage callback via setState. The pattern is deterministic: mount creates the Worker, unmount destroys it, and every render sees the same ref object.
The three-hook pattern is effectively a resource lifecycle manager: useRef for stable identity, useEffect for setup and teardown, useState for reactivity. This same structure applies to any external resource that outlives a single render — WebSockets, observers, or WASM instances.
Storing a Worker instance in useState is a common mistake because it causes unnecessary re-renders and risks losing the reference on state updates. useRef is the correct choice precisely because it sidesteps React's reactivity system for values that don't belong to the UI.
The cleanup function in useEffect is not optional for Workers. A terminated Worker frees its thread; an unterminated one persists until the tab closes, which matters in single-page apps where components mount and unmount frequently.