Watchstop
00:00.00
Frameworks

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): StopwatchBinding

useStopwatch

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 the subscribe bridge only after the component is visible on the client, so SSR does not leak subscriptions.
  • Task cleanup unsubscribes and calls destroy() when the task is disposed (component unmount / re-run).
  • elapsed is a Qwik Signal<number>. stopwatch is the owned instance, exposed for passing elsewhere; do not call destroy() on it yourself.

Options

OptionTypePurpose
clockClockOwned mode: use this clock instead of detectClock(). Pass createMockClock() in tests.
precisionMsnumberOwned mode: coarsen notify cadence — see Options.
stopwatchStopwatchBorrowed 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 client subscribe bridge.
  • 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.

See Store and Spec.

On this page