Skip to content

use_state

use_state gives a component a value that persists across renders, and a setter that changes it. Setting a different value re-renders the component.

@component
def counter() -> Div:
    count, set_count = use_state(0)

    def on_input(event: InputEvent, _: Receiver) -> InputResponse:
        match event:
            case KeyPressed(key="+"):
                set_count(lambda count: count + 1)
                return ()
            case KeyPressed(key="-"):
                set_count(lambda count: count - 1)
                return ()
            case KeyPressed(key="0"):
                set_count(0)
                return ()
            case _:
                return None

    return Div(
        style=col | border | border_lightrounded | pad_x(1),
        on_input=on_input,
        children=(
            Text(content=f"Count: {count}", style=text_color("amber", 400)),
            Text(content="+ and - count, 0 resets", style=text_color("slate", 400)),
        ),
    )

A counter showing a count of 3

╭────────────────────────────╮
│ Count: 3                   │
│ + and - count, 0 resets    │
╰────────────────────────────╯

Each key press sets the count, which renders counter again with the new value:

+ and - pass the setter a function, since the new count depends on the old one (see below), and 0 passes it a value, since the reset doesn't.

Updating state from its previous value

When the new state depends on the old, pass the setter a function

count is the value as of the render that built the handler, and nothing updates it until the next render. A setter given a value replaces the state with it, so every update computed from count before the next render starts from the same stale count, and only the last one survives. No error is raised: the other updates just vanish.

Given a function, the setter calls it right away with the latest state, updates included, and stores what it returns:

set_count(count + 1)            # wrong: loses updates made since this render
set_count(lambda c: c + 1)      # right: builds on the latest state

Several updates can land before the next render in ways that are easy to miss, because each looks like one update when you read the code.

Two updates in one handler. Each by_value + 1 is computed from the same by_value, while each function builds on what the one before it returned:

@component
def plus_two() -> Div:
    by_value, set_by_value = use_state(0)
    by_function, set_by_function = use_state(0)

    def on_input(event: InputEvent, _: Receiver) -> InputResponse:
        match event:
            case KeyPressed(key="+"):
                set_by_value(by_value + 1)
                set_by_value(by_value + 1)
                set_by_function(lambda n: n + 1)
                set_by_function(lambda n: n + 1)
                return ()
            case _:
                return None

    return Div(
        style=col | border | border_lightrounded | pad_x(1),
        on_input=on_input,
        children=(
            Text(content=f"By value:    {by_value}", style=text_color("red", 400)),
            Text(content=f"By function: {by_function}", style=text_color("emerald", 400)),
        ),
    )

After three presses of +, by value shows 3 and by function shows 6

╭────────────────────────────╮
│ By value:    3             │
│ By function: 6             │
╰────────────────────────────╯

After three presses of +, by_value has gone up by one a press, and by_function by two.

Key presses that arrive together. Counterweight routes every event it has read before rendering again, and a terminal delivers keys together whenever they come faster than one per frame: fast typing, a held key repeating, or text pasted into a terminal without bracketed paste. Each key press's handler sees the same text:

text, set_text = use_state("")


def on_input(event: InputEvent, _: Receiver) -> InputResponse:
    match event:
        case KeyPressed(key=key) if len(key) == 1:
            set_text(text + key)  # "abc" typed in one burst leaves text as "c"
            return ()
        case _:
            return None

This one hides from tests: an autopilot renders after each event it sends, so every key press there sees the latest text, and so does slow typing by hand. Write set_text(lambda t: t + key). For a text field, use_text_input does this for you, along with the cursor, the editing keys and pastes.

An effect that outlives its render. An effect closes over the values from the render that started it, and one with deps=() keeps them for as long as the component is mounted:

count, set_count = use_state(0)


async def tick() -> None:
    while True:
        await asyncio.sleep(1)
        set_count(count + 1)  # count stays at 1, since this count is always 0


use_effect(tick, deps=())

Pass a value only when the new state doesn't depend on the old, as in set_query("") to clear a field or set_selected(item.id) to select an item. Toggling a flag depends on it, so it takes a function too: toggle, as in set_open(toggle).

The setter is the same object on every render, so a handler that updates state only through functions captures nothing that goes stale, and can be built once with use_memo or passed to a memo component.

Deciding from the latest state

A function passed to the setter moves the state along, but a handler often has to decide something from the state as well: whether to claim a key or let it bubble, which controls to return, or which option to hand its parent. Those decisions are made when the handler returns, before the next render, so they can't be left to a render or an effect.

The setter returns the state as it settled, the value it was given or what the function returned, and get_state reads the latest state without changing it. A handler that decides from the state reads it once, at the top, since nothing else changes it between that read and the handler's own writes:

board, set_board = use_state(Board())


def on_input(event: InputEvent, _: Receiver) -> InputResponse:
    match event:
        case KeyPressed(key=Key.Enter):
            before = get_state(set_board)
            return Bell() if set_board(Board.guessed) == before else ()
        case _:
            return None

Enter rings the bell when the board refuses the guess, which the handler learns from what the setter settled at, even when the keys before it arrived in the same batch and no render has shown their board yet.

Since the setter returns the state, a handler written as a lambda whose body is a setter call returns that state as its response, which isn't one a handler can return. Write the handler as a function that says whether it claims the event: () claims it, and None lets it bubble.

API

counterweight.hooks.use_state

use_state(
    initial_value: Getter[T],
) -> tuple[T, Setter[T]]
use_state(initial_value: T) -> tuple[T, Setter[T]]
use_state(
    initial_value: Getter[T] | T,
) -> tuple[T, Setter[T]]
PARAMETER DESCRIPTION
initial_value

The initial value of the state. It can either be the initial value itself, or a zero-argument function that returns the initial value.

TYPE: Getter[T] | T

RETURNS DESCRIPTION
T

The current value of the state (i.e., for the current render cycle).

Setter[T]

A function that can be called to update the value of the state (e.g., in an event handler). It can either be called with the new value of the state, or a function that takes the current value of the state and returns the new value of the state. When the new value depends on the current one, pass a function: a value computed from the state a render saw loses every update made since, such as from other key presses read before the next render (see Updating state from its previous value). If the value is not equal to the current of the state, Counterweight will trigger a render cycle. It returns the state as it settled: the value it was given, or what the function returned, so a handler can read the latest state with get_state. It is the same object on every render, so it can be left out of an effect's deps, and including it never reruns the effect. Once the component unmounts, calling it changes nothing, and returns the state as it was when the component unmounted.

counterweight.hooks.get_state

get_state(set_state: Editor[T]) -> T

The state as every update since the last render left it, read by calling set_state with a function that returns the state it's given, so it changes nothing and queues no render. set_state is a use_state setter, or any Editor, such as a parent's setter wrapped to edit one field of its state. Several key presses can arrive before the next render, and each runs a handler the last render made, so a handler that decides something from the state (whether to claim a key, which option to choose) reads it here, where the value use_state returned is the one that render saw. It's for handlers and effects; a render reads the value use_state returned.

counterweight.hooks.toggle

toggle(value: bool) -> bool

Flips a flag: pass it to a use_state setter, as set_open(toggle), so each toggle flips the latest value, even when several land before the next render. It takes only a bool, so a type checker rejects it for state of any other type, where operator.not_ would quietly turn the state into one.

counterweight.hooks.Getter

Getter = Callable[[], T]

A function that computes a value, such as the initial state use_state computes only on the first render.

counterweight.hooks.Setter

Setter = Callable[[Callable[[T], T] | T], T]

What use_state returns to set its state: given a value, the next state, or given a function, the next state computed from the latest one. It returns the state as it settled: the value it was given, or what the function returned.

counterweight.hooks.Editor

Editor = Callable[[Callable[[T], T]], T]

The half of a Setter that takes a function: given a function from the latest value to the next, it applies it and returns the value it settled at. A use_state setter is one, and so is a wrapper around one that validates or transforms each edit, such as lambda edit: set_value(lambda v: new if (new := edit(v)).isdigit() or not new else v), which never has to handle a plain value, since nothing passes it one.