Skip to content

use_clock

use_clock gives a component the time, so it can show a clock, an uptime, or a timer while staying pure. A component that reads the time any other way, such as with datetime.now(), keeps showing the time from whenever it last ran.

@component
def wall_clock() -> Text:
    tick = use_clock(timedelta(minutes=1))
    return Text(content=f"{tick.now:%H:%M}", style=text_color("amber", 400))


@component
def uptime() -> Text:
    tick = use_clock(timedelta(seconds=1))
    seconds = tick.since_start // timedelta(seconds=1)
    return Text(content=f"Up {seconds // 3600}:{seconds // 60 % 60:02}:{seconds % 60:02}")


@component
def status_bar() -> Div:
    return Div(
        style=row | justify_children_space_between | border | border_lightrounded | pad_x(1),
        children=[uptime(), wall_clock()],
    )

Status bar

╭──────────────────────────────────────╮
│ Up 1:02:05                     16:11 │
╰──────────────────────────────────────╯

Ticks

Each call reads the clock every interval. A component takes a reading when it mounts, and again on its first render after each new tick starts, re-running then if nothing else made it. Every render in between gets the same reading, so re-running the component for any other reason doesn't change what it shows.

Ticks start at whole multiples of interval on the wall clock, so wall_clock re-runs as the minute turns, along with uptime, rather than a minute after the app started. Components that take a reading in the same frame get the same one, so two clocks on screen never disagree.

Each tick re-runs the components that read the clock and nothing else, so call use_clock in the component that shows the time, not in one that also renders things that don't change with it. See Performance.

Timing from an event

since_mount is how long the component has been mounted. To time something from an event, such as a key press, render a component that reads the clock and give it a new key when the event happens, which remounts it and starts its since_mount again from zero:

@component
def lap_time() -> Text:
    elapsed = use_clock(timedelta(seconds=1)).since_mount
    return Text(content=f"{elapsed // timedelta(seconds=1)} s")


@component
def lap_timer() -> Div:
    laps, set_laps = use_state(1)

    def on_key(event: KeyPressed) -> None:
        if event.key == Key.Space:
            set_laps(laps + 1)

    return Div(
        style=row | gap(1),
        on_key=on_key,
        children=[
            Text(content=f"Lap {laps}:", style=text_color("slate", 400)),
            # A new key each lap remounts lap_time, so its time since mounting starts again from zero.
            lap_time().with_key(str(laps)),
        ],
    )

Lap timer

Lap 2: 42 s         

A handler sees the reading from the latest tick, so a time it records, such as the lap time when a lap ends, is up to one interval early. Choose an interval as fine as the precision the time needs. A timer that can be paused and resumed loses up to that much on each pause, since it carries the time recorded at the pause into the next run.

Why compute from elapsed time?

Compute whatever changes over time, such as an animation frame or a countdown, from since_mount or since_start. Don't keep a running total in state and add to it each tick:

@component
def spinner() -> Text:
    frame, set_frame = use_state(0)

    async def step() -> None:
        while True:
            await sleep(0.1)
            set_frame(lambda frame: frame + 1)  # wrong: falls further behind each step

    use_effect(step, deps=())

    return Text(content="|/-\\"[frame % 4])

Each step counts a full interval, but takes longer: sleep wakes up late, and a render and any other work run before the next step starts. The total falls behind the real elapsed time by a little more each step, and the gap only grows. Measured on an otherwise idle event loop, steps of 10 ms counted 2 seconds when 2.05 had passed. Adding up the real time between steps instead fixes the drift, but anything that misses a step or repeats one, such as an effect that restarts, still breaks the total.

Elapsed time from use_clock is the difference between two readings of a monotonic clock, so a value computed from it can't drift, however long the component runs:

@component
def spinner() -> Text:
    frame = use_clock(timedelta(milliseconds=100)).since_mount // timedelta(milliseconds=100)
    return Text(content="|/-\\"[frame % 4])

It also needs no state or effect, so a tick re-renders once instead of twice.

Testing

Pass a frozen_clock to app to fix the time, and move it on with AdvanceClock in the autopilot, so a test or a screenshot doesn't depend on when it runs or how long it takes:

async def test_the_lap_timer_counts_from_the_last_lap() -> None:
    await app(
        lap_timer(),
        headless=True,
        clock=frozen_clock(datetime(2026, 3, 14, 15, 9, 26)),
        autopilot=[
            AdvanceClock(by=timedelta(seconds=75)),
            KeyPressed(key=Key.Space),
            AdvanceClock(by=timedelta(seconds=42)),
            Screenshot.to_file(Path("lap-timer.txt")),
            Quit(),
        ],
        check_purity=True,
    )

API

counterweight.hooks.use_clock

use_clock(interval: timedelta) -> Tick
PARAMETER DESCRIPTION
interval

How often the component reads the clock. Ticks start at whole multiples of interval past midnight on the wall clock (for an interval that divides a day), so components reading the clock every second and every minute both re-run as the minute turns.

TYPE: timedelta

RETURNS DESCRIPTION
Tick

A reading of the app's clock, taken when the component mounts, and again on the first render after each new tick starts, with the component re-running then if nothing else made it. Between those, every render returns the same reading, so the component's output stays a function of its arguments and hooks. Components that take a reading in the same frame get the same one, whatever their intervals.

RAISES DESCRIPTION
ValueError

If interval isn't positive.

counterweight.hooks.Tick dataclass

Tick(
    now: datetime,
    since_start: timedelta,
    since_mount: timedelta,
)

One reading of the app's clock, which use_clock takes for a component when it mounts and again on its first render in each new tick. Every field comes from the same reading, and every duration is measured on a monotonic clock, so changes to the wall clock don't make them jump.

now instance-attribute

now: datetime

The wall-clock time, in the clock's timezone: the local one unless the app is given another clock.

since_start instance-attribute

since_start: timedelta

How long the app had been running.

since_mount instance-attribute

since_mount: timedelta

How long the component had been mounted: zero on the reading taken when it mounts.

counterweight.clock.frozen_clock

frozen_clock(wall: datetime) -> Clock

A clock that always reads wall, for tests and screenshots that mustn't depend on when they run. Advance it with the AdvanceClock control.

counterweight.clock.system_clock

system_clock() -> Instant

The system's clock, with the wall-clock time in the local timezone.

counterweight.clock.Instant dataclass

Instant(wall: datetime, monotonic: timedelta)

One reading of a clock: the wall-clock time, and a monotonic time read along with it.

wall instance-attribute

wall: datetime

monotonic instance-attribute

monotonic: timedelta

Unaffected by changes to the wall clock. Only the difference between two instants from one clock means anything.

__add__

__add__(other: timedelta) -> Instant