Skip to content

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

tsx
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

tsx
import { useState } from 'react';
import { useCountdown } from '@timekeeper-countdown/react';

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(Number(event.target.value) || 0)} />
      </label>

      <div>
        <button onClick={countdown.start}>Start</button>
        <button onClick={countdown.pause}>Pause</button>
        <button onClick={() => countdown.reset(seconds)}>Apply</button>
      </div>

      <p>{countdown.totalSeconds}s remaining</p>
    </section>
  );
}

Auto-Chaining Phases

tsx
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

tsx
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 renderHook or 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 when setInterval callbacks fire.

ts
vi.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

ts
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)

ts
import { CountdownEngine } from '@timekeeper-countdown/core';
import {
  createFakeTimeProvider,
  toTimeProvider,
  assertRemainingSeconds,
  assertSnapshotCompleted,
  TimerState,
} from '@timekeeper-countdown/core/testing-utils';

const fake = createFakeTimeProvider({ startMs: 0, tickMs: 1000 });
const engine = CountdownEngine(5, {
  timeProvider: toTimeProvider(fake),
  tickIntervalMs: 5,
});

engine.start();
fake.advance(3000); // advance 3 seconds
assertRemainingSeconds(engine.getSnapshot(), 2);

fake.advance(2000); // advance 2 more seconds
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.