Qwik
@watchstop/qwik adapter.
Adapter bridging Store into a Qwik signal, with subscribe only on the client via useVisibleTask$. Peer dependency is @qwik.dev/core (Qwik 2 beta), not @builder.io/qwik. @watchstop/core is also a peer — install both.
This package is published as a Qwik library: build with vite build --mode lib so the optimizer emits index.qwik.mjs / .cjs, and package.json exposes a "qwik" field pointing at that entry. Apps import @watchstop/qwik normally — do not import package src.
The owned Stopwatch is held with noSerialize() inside a signal so QRL closures (visible task and controls) can capture a serializable holder. Controls are $() QRLs safe for onClick$={start}; handlers that need custom logic should call methods on the exposed stopwatch instance (also noSerialize) instead of nesting QRL invokes.
Exact public names
useStopwatch is the entire public API. The name follows Qwik's hooks idiom (same as React / Vue / Solid).
type UseStopwatchOptions =
| { clock?: Clock; precisionMs?: number }
| { stopwatch: Stopwatch }
type StopwatchBinding = {
elapsed: Signal<number>
running: Signal<boolean>
start: QRL<() => void>
stop: QRL<() => void>
reset: QRL<() => void>
stopwatch: Stopwatch
}
declare function useStopwatch(options?: UseStopwatchOptions): StopwatchBindinguseStopwatch
useStopwatch owns a Stopwatch and its teardown, so a component that needs its own timer imports one thing and holds no instance itself.
import { component$ } from '@qwik.dev/core'
import { useStopwatch } from '@watchstop/qwik'
export const Timer = component$(() => {
const { elapsed, running, start, stop, reset } = useStopwatch()
return (
<>
<p>{elapsed.value} ms</p>
<button onClick$={running.value ? stop : start}>
{running.value ? 'Stop' : 'Start'}
</button>
<button onClick$={reset}>Reset</button>
</>
)
})- Construction is inert. Nothing is scheduled until
start(). useVisibleTask$registers thesubscribebridge only after the component is visible on the client, so SSR does not leak subscriptions.- Task
cleanupunsubscribes and callsdestroy()when the task is disposed (component unmount / re-run). elapsedis a QwikSignal<number>.stopwatchis the owned instance, exposed for passing elsewhere; do not calldestroy()on it yourself.
Options
| Option | Type | Purpose |
|---|---|---|
clock | Clock | Owned mode: use this clock instead of detectClock(). Pass createMockClock() in tests. |
precisionMs | number | Owned mode: coarsen notify cadence — see Options. |
stopwatch | Stopwatch | Borrowed mode: bind this instance; do not pass clock / precisionMs. |
Sharing one stopwatch across components
Pass the same core instance into each hook:
import { component$ } from '@qwik.dev/core'
import { Stopwatch } from '@watchstop/core'
import { useStopwatch } from '@watchstop/qwik'
const session = new Stopwatch()
export const SessionChip = component$(() => {
const { elapsed, running, start, stop, reset } = useStopwatch({
stopwatch: session,
})
// ...
})The adapter never calls destroy() on a borrowed instance. Own teardown yourself when the session ends, or leave a module-level instance alive for the page lifetime.
Contract
- The signal starts at
store.get()and is written only from the clientsubscribebridge. - Subscribe and destroy run inside
useVisibleTask$, not during SSR task execution. - Controls stay on the
Stopwatch.
Re-render cost
elapsed is raw milliseconds delivered at the clock's tick cadence, so anything reading the signal re-renders roughly 60 times a second under createBrowserClock. Pass precisionMs to coarsen notifies — see Options. Keep the elapsed read in the smallest possible component when you still want finer UI.