I am trying to create a performant timer component in React.js (for learning purposes) and for that I am using web workers. The logic works as expected the first time when I click “Start” button but some weird race like condition starts appearing in countdown when I stop the counter and start again and then stop, only this time it won’t stop and timer speed accelerates!
Live code at: https://stackblitz.com/edit/timer-worker-wfs7e3
Trying to explain the process step by step:
Step | Action | Button State | Timer State | Note |
---|---|---|---|---|
1 | Clicks “Start” | Text updates to “Stop”. Color to “red” | Active | Timer starts as expected with speed as expected |
2 | Clicks “Stop” | Text updates to “Start”. Color to “blue” | Inactive | Timer resets as expected |
3 | Clicks “Start” | Text updates to “Stop”. Color to “red” | Active | Timer starts as expected with speed as expected |
4 | Clicks “Stop” | Text updates to “Start”. Color to “blue” | Active! | Timer resets to 0 but doesn’t stop and starts running slightly faster |
5 | Clicks “Start” | Text updates to “Stop”. Color to “red” | Active! | Timer resets to 0 but doesn’t stop and starts running even more faster |
And so on. This keeps on happening until I refresh my page.
By the looks of it, seems that that there’s some race condition going on ( I maybe wrong) And I am unable to figure out the root cause. Is it bug or simply a failure at logical level?
// /src/workers/timer-worker.ts
const timerWorker = () => {
let seconds = 0
let timerId: number | null = null
const INTERVAL = 100
function dataFactory(time: number, status: "start" | "stop") {
return { time, status }
}
self.onmessage = (event) => {
if (event.data === "start") {
seconds = 0
timerId = setInterval(() => {
seconds++
self.postMessage(dataFactory(seconds, "start"))
}, INTERVAL)
}
if (event.data === "stop") {
seconds = 0
self.postMessage(dataFactory(seconds, "stop"))
clearInterval(timerId as number)
}
}
}
export default timerWorker
// /src/workers/web-worker.ts
export default class WebWorker {
constructor(worker: any) {
const code = worker.toString();
const blob = new Blob(['('+code+')()']);
return new Worker(URL.createObjectURL(blob));
}
}
// src/components/Timer.tsx
import { FC, useEffect, useRef, useMemo, useState } from "react"
import { Timer as TimerStatus } from "../enums/timer"
import TimerWorker from "../worker/timer-worker"
import WebWorker from "../worker/web-worker"
export interface TimerProps {}
const Timer: FC<TimerProps> = () => {
const [timerRunning, setTimerRunning] = useState(false)
const [seconds, setSeconds] = useState(0)
const workerRef = useRef<Worker | null>(null)
const timer = useMemo(() => {
const secs = seconds % 60
const mins = Math.floor(seconds / 60) % 60
const hrs = Math.floor(seconds / 3600) % 24
const days = Math.floor(seconds / (3600 * 24))
return `${days}d ${hrs}h ${mins}m ${secs}s`
}, [seconds])
const btnStyle = `btn ${timerRunning ? "btn-danger" : ""}`
function initTimerWorker() {
workerRef.current = new WebWorker(TimerWorker)
workerRef.current.onmessage = (event) => {
console.log(event.data)
const { time, status } = event.data
console.log(time, status)
setSeconds(time)
}
// useEffect cleanup
return () => workerRef.current?.terminate()
}
function clickHandler() {
setTimerRunning((prev) => {
const newState = !prev
workerRef.current?.postMessage(
newState ? TimerStatus.START : TimerStatus.STOP
)
return newState
})
}
useEffect(initTimerWorker, [])
return (
<div
id="Timer"
className="place-self-center bg-white rounded-md p-4 w-80 text-center border border-solid border-slate-200"
>
<div className="text-3xl mb-2 font-bold">{timer}</div>
<button className={btnStyle} onClick={clickHandler}>
{timerRunning ? "Stop" : "Start"}
</button>
</div>
)
}
export default Timer
// /src/enums/timer.ts
export enum Timer {
START = "start",
STOP = "stop"
}
Live code at: https://stackblitz.com/edit/timer-worker-wfs7e3