Examples
Copy-and-paste snippets for @timekeeper-countdown/react. When new framework adapters ship we will add dedicated examples for them as well.
Basic Timer Card
import { useCountdown } from '@timekeeper-countdown/react';
import { formatTime } from '@timekeeper-countdown/core/format';
export function TimerCard() {
const countdown = useCountdown(60);
const clock = formatTime(countdown.snapshot);
return (
<div>
<p>
{clock.minutes}:{clock.seconds}
</p>
<button onClick={countdown.start} disabled={countdown.isRunning}>
Start
</button>
<button onClick={countdown.pause} disabled={!countdown.isRunning}>
Pause
</button>
<button onClick={countdown.reset}>Reset</button>
</div>
);
}Form-Controlled Duration
Durations must be non-negative integers — setSeconds/reset throw on a negative, non-integer, NaN, or Infinity value (see the API Reference). Sanitize the raw <input> value at the boundary so a decimal like 1.5 can never reach the engine.
import { useState } from 'react';
import { useCountdown } from '@timekeeper-countdown/react';
// Coerce any raw input into a safe, non-negative integer.
const toSafeSeconds = (raw: string) => Math.max(0, Math.floor(Number(raw) || 0));
function AdjustableCountdown() {
const [seconds, setSeconds] = useState(150);
const countdown = useCountdown(seconds, { autoStart: false });
return (
<section>
<label>
Seconds
<input type="number" value={seconds} onChange={event => setSeconds(toSafeSeconds(event.target.value))} />
</label>
<div>
<button onClick={countdown.start}>Start</button>
<button onClick={countdown.pause}>Pause</button>
<button onClick={() => countdown.reset(Math.max(0, Math.floor(seconds)))}>Apply</button>
</div>
<p>{countdown.totalSeconds}s remaining</p>
</section>
);
}Auto-Chaining Phases
import { useEffect } from 'react';
import { useCountdown } from '@timekeeper-countdown/react';
function TwoStageFlow() {
const intro = useCountdown(15, { autoStart: true });
const main = useCountdown(90);
const { isCompleted: introCompleted } = intro;
const { isRunning: mainRunning, start: startMain } = main;
useEffect(() => {
if (introCompleted && !mainRunning) {
startMain();
}
}, [introCompleted, mainRunning, startMain]);
return (
<div>
<h3>Intro: {intro.totalSeconds}s</h3>
<h3>Main Session: {main.totalSeconds}s</h3>
</div>
);
}Testing with @testing-library/react
import { useMemo } from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { useCountdown } from '@timekeeper-countdown/react';
import { createFakeTimeProvider, toTimeProvider } from '@timekeeper-countdown/core/testing-utils';
function InspectableTimer() {
const fake = useMemo(() => createFakeTimeProvider({ startMs: 0 }), []);
const countdown = useCountdown(5, {
timeProvider: toTimeProvider(fake),
tickIntervalMs: 5,
});
return (
<div>
<output>{countdown.totalSeconds}</output>
<button onClick={() => fake.advance(1000)}>Advance</button>
</div>
);
}
it('advances when the fake clock moves', async () => {
render(<InspectableTimer />);
await userEvent.click(screen.getByRole('button', { name: /advance/i }));
expect(screen.getByText('4')).toBeInTheDocument();
});Note: When testing with
renderHookor custom render functions that use Vitest fake timers (vi.useFakeTimers()), you may need to advance both the fake time provider AND Vitest's internal timers. The fake provider controls what time the engine reads; Vitest fake timers control whensetIntervalcallbacks fire.tsvi.useFakeTimers({ toFake: ['setTimeout', 'setInterval'] }); const fake = createFakeTimeProvider({ startMs: 0 }); // Inside your test: act(() => { fake.advance(1000); // engine reads 1s elapsed vi.advanceTimersByTime(1000); // triggers setInterval callbacks });
Testing with Core Utilities
Snapshot fabrication for unit tests
import {
buildSnapshot,
buildSnapshotSequence,
assertSnapshotState,
assertSnapshotCompleted,
assertRemainingSeconds,
TimerState,
} from '@timekeeper-countdown/core/testing-utils';
// Create an isolated snapshot
const idle = buildSnapshot({ totalSeconds: 60 });
assertSnapshotState(idle, TimerState.IDLE);
// Create a completed snapshot
const done = buildSnapshot({ totalSeconds: 0, state: TimerState.STOPPED });
assertSnapshotCompleted(done);
// Verify with tolerance
const mid = buildSnapshot({ totalSeconds: 30, state: TimerState.RUNNING });
assertRemainingSeconds(mid, 30);
// Generate a sequence
const sequence = buildSnapshotSequence({ totalSeconds: 10, step: 5, count: 3 });
// [10s RUNNING, 5s RUNNING, 0s STOPPED]Deterministic test with engine (plain core, no React)
import { vi } from 'vitest';
import { CountdownEngine } from '@timekeeper-countdown/core';
import {
createFakeTimeProvider,
toTimeProvider,
assertRemainingSeconds,
assertSnapshotCompleted,
} from '@timekeeper-countdown/core/testing-utils';
// Deterministic engine tests advance the injected clock AND fire the engine's
// internal interval with your runner's fake timers (Vitest shown), because
// `getSnapshot()` only refreshes on a tick or a state transition.
vi.useFakeTimers();
const fake = createFakeTimeProvider({ startMs: 0, tickMs: 1000 });
const engine = CountdownEngine(5, {
timeProvider: toTimeProvider(fake),
tickIntervalMs: 10,
});
engine.start();
fake.advance(3000); // advance the injected clock 3 seconds
vi.advanceTimersByTime(10); // fire the interval so getSnapshot() refreshes
assertRemainingSeconds(engine.getSnapshot(), 2);
fake.advance(2000); // 2 more seconds -> reaches zero
vi.advanceTimersByTime(10); // drive the final tick -> completion
assertSnapshotCompleted(engine.getSnapshot());Coming Soon
Adapters for Angular, Vue, Svelte, and a vanilla bundle are in development. As they land, this page will grow with side-by-side examples so you can port patterns across frameworks with minimal effort.