Webworker React Hook
import { useCallback, useEffect, useRef, useState } from 'react'
/** Worker Message Payload */
type MessagePayload<TResponse> = {
/** message data */
data: TResponse | undefined
/** error */
error?: Error | string
}
/** Webworker Config */
type WebWorkerConfig = {
/** worker url */
url?: string
/** webworker context */
worker?: Worker
}
/** Worker Container Ref */
type WorkerContainerRef = {
/** worker */
worker?: Worker
/** to determine to terminate the worker or not */
shouldTerminate?: boolean
}
/**
* ### Webworker Hook
* @param config - webworker config
* @example
* import FibonacciWorker from '../../../workers/fibonacci-worker?worker'
* // define worker input type
* type WorkerInput = {
* input: number;
* }
*
* // define worker response type
* type WorkerResponse = {
* result: number;
* }
*
* // hook usage
* * const [data, postData, error] = useWebWorker<WorkerInput, WorkerResponse>({
* worker: new FibonacciWorker(),
* })
*
* // handle action
* const handleAction = (input: number) => {
* postData({ input })
* }
*
* // response and error
* console.log("Error >> ", error)
* console.log("Result >> ", data)
*
* @version 1.0.0
* @author Thuta
*/
export function useWebWorker<TData, TResponse>(config: WebWorkerConfig) {
// ✔ webworker config
const { url, worker: workerContext } = config
/** worker container */
const workerContainer = useRef<WorkerContainerRef>({})
// ✔ message payload state
const [message, setMessage] = useState<MessagePayload<TResponse>>({
data: undefined,
error: undefined,
})
/**
* onMessage Handler
* @param payload - message payload
*/
const onMessage = useCallback((payload: MessageEvent<TResponse>): void => {
const { data } = payload
setMessage((prev) => {
if (prev.data !== data) {
return {
data,
error: undefined,
}
}
return prev
})
}, [])
/**
* onError Handler
* @param errorEvent - error event
*/
const onError = useCallback((errorEvent: ErrorEvent): void => {
setMessage({
data: undefined,
error: errorEvent.message || new Error('Worker encountered an error'),
})
}, [])
/**
* Post data to the WebWorker
* @param data - message data
*/
const postData = useCallback((data: TData): void => {
const { worker } = workerContainer.current
if (!worker) {
setMessage({
data: undefined,
error: new Error('Worker not initialized'),
})
} else {
worker.postMessage(data)
}
}, [])
/**
* Create or retrieves a WebWorker
*/
const createWebWorker = (): Worker | undefined => {
const { current } = workerContainer
if (url) {
current.worker = new Worker(url)
current.shouldTerminate = true // should terminate since it was created internally
} else if (workerContext) {
current.worker = workerContext
current.shouldTerminate = false // don't terminate external worker
}
return current.worker
}
/**
* Set up the WebWorker and add event listeners
*/
const setupWebWorker = (): void => {
const worker = createWebWorker()
if (!worker) {
setMessage({
data: undefined,
error: 'Either Worker URL or Worker instance must be provided',
})
return
}
worker.addEventListener('message', onMessage)
worker.addEventListener('error', onError)
}
/**
* Clean up WebWorker: remove event listeners and terminate if needed
*/
const terminateWebWorker = (): void => {
const { worker, shouldTerminate } = workerContainer.current
if (worker) {
worker.removeEventListener('message', onMessage)
worker.removeEventListener('error', onError)
if (shouldTerminate) {
worker.terminate()
}
workerContainer.current = {}
}
}
// ✔ Setup WebWorker Effect
useEffect(() => {
setupWebWorker()
return () => {
terminateWebWorker()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [url, workerContext, onMessage, onError])
return [message.data, postData, message.error] as const
}