Skip to content

Queueing a Series of State Updates

Overview

Setting a state variable will queue another render. But sometimes you might want to perform multiple operations on the value before queueing the next render. To do this, it helps to understand how ReactPy batches state updates.

You will learn

  • How ReactPy batches state updates
  • Updating the same state multiple times before the next render
  • What happens when you mix values and updater functions

ReactPy batches state updates

You might expect that clicking the "+3" button will increment the counter three times, because it calls set_number(number + 1) three times:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@component
def counter():
    number, set_number = use_state(0)

    def handle_click(event):
        set_number(number + 1)
        set_number(number + 1)
        set_number(number + 1)

    return html.div(
        html.h1(number),
        html.button({"onClick": handle_click}, "+3"),
    )
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
button {
    display: inline-block;
    margin: 10px;
    font-size: 20px;
}

h1 {
    display: inline-block;
    margin: 10px;
    width: 30px;
    text-align: center;
}
1
# TODO

It lands on 1, not 3. Two things combine to produce that.

First, as you may recall from State as a Snapshot, each render's state values are fixed. Inside this render's event handler number is always 0, no matter how many times you call the setter, so all three lines evaluate to the same call:

set_number(0 + 1)
set_number(0 + 1)
set_number(0 + 1)

Second, ReactPy waits until all code in a synchronous event handler has run before rendering. The re-render happens only after the handler returns, so the three assignments collapse into one render showing 1.

This lets you update several state variables — even across several components — without triggering a render for each. It also avoids "half-finished" renders in which only some variables have been updated.

Batching stops at an await

The batching above is a side effect of a synchronous handler, and it is narrower than React's. ReactPy's setter applies its value to the hook's state immediately and merely flags the component for re-render (core/hooks.py, _CurrentState.dispatch), so an await gives the scheduler a chance to render what has been set so far.

Measured in a real browser, watching every value the counter takes after a single click:

Handler Observed sequence
three set_number(n => n + 1) in a def 0 → 3 (one render)
the same, with await asyncio.sleep(0.3) between the calls 0 → 1 → 2 → 3 (three renders)

React 18+ would batch both into a single render. In ReactPy, an async def handler that awaits between updates renders — and repaints — in between. If that flicker matters, do the awaits first and set state once at the end, or collect the results and make a single setter call.

ReactPy does not batch across multiple intentional events like clicks — each click is handled separately. This ensures that, for example, if the first click disables a form, the second click cannot submit it again.

Updating the same state multiple times before the next render

It is an uncommon use case, but if you would like to update the same state variable multiple times before the next render, pass a function that computes the next value from the previous one instead of the value itself: set_number(lambda n: n + 1). This is a way of saying "do something with the current state" rather than "replace the state".

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@component
def counter():
    number, set_number = use_state(0)

    def handle_click(event):
        set_number(lambda n: n + 1)
        set_number(lambda n: n + 1)
        set_number(lambda n: n + 1)

    return html.div(
        html.h1(number),
        html.button({"onClick": handle_click}, "+3"),
    )
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
button {
    display: inline-block;
    margin: 10px;
    font-size: 20px;
}

h1 {
    display: inline-block;
    margin: 10px;
    width: 30px;
    text-align: center;
}
1
# TODO

This time the counter reaches 3, because each updater receives whatever the previous one produced.

What happens if you update state after replacing it

What does this handler produce?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@component
def counter():
    number, set_number = use_state(0)

    def handle_click(event):
        # number is 0 for this render, so this reads as set_number(5).
        set_number(number + 5)
        set_number(lambda n: n + 1)

    return html.div(
        html.h1(number),
        html.button({"onClick": handle_click}, "Increase the number"),
    )
1
# TODO

Step by step, starting from number == 0:

  1. set_number(number + 5) — number is 0, so this is set_number(5). The state becomes 5.
  2. set_number(lambda n: n + 1) — an updater, so it runs against the current value: 5 + 1.

The next render shows 6, confirmed by rendering the example.

set_number(5) behaves like set_number(lambda n: 5)

A plain value ignores the argument entirely — it just replaces. That equivalence is what makes the next case surprising.

What happens if you replace state after updating it

One more:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@component
def counter():
    number, set_number = use_state(0)

    def handle_click(event):
        set_number(number + 5)
        set_number(lambda n: n + 1)
        set_number(42)

    return html.div(
        html.h1(number),
        html.button({"onClick": handle_click}, "Increase the number"),
    )
1
# TODO
  1. set_number(number + 5) — number is 0, so state becomes 5.
  2. set_number(lambda n: n + 1) — updater, so 5 + 1 = 6.
  3. set_number(42) — a plain value, so state becomes 42.

The result is 42. The two updaters still ran, and their work was then overwritten — which is the same answer React's queue produces, arrived at by a different route.

To summarize what you can pass to a state setter:

  • An updater function (e.g. lambda n: n + 1) is applied to the current value.
  • Any other value (e.g. 5) replaces the state outright, discarding whatever the updaters produced.

Updater functions run while your handler runs, so — like components — they must be pure. Don't set state from inside one or perform side effects there. Because they can run at any point, an updater that reads a variable from the enclosing render may see a stale value; take everything you need from the n argument.

An updater that raises fails silently

Updaters run eagerly, inside the setter call itself (core/hooks.py, _CurrentState.dispatch calls new(self.value) before scheduling anything) — not later during rendering. Verified: an updater's side effect lands before the line after its set_number(...).

That has a nasty consequence. If an updater raises, the state is never assigned and no render is scheduled; ReactPy logs Failed to execute event handler on the server and swallows the error. The browser shows nothing, the console stays quiet, and the component simply stops responding to that button. Reproduced: a lambda x: 1 / 0 updater produced a server-side traceback and then an indefinite absence of any render.

Two habits follow: keep updaters to one pure expression, and when a counter "stops working" with no error in the browser, check the server log for Failed to execute event handler.

Naming conventions

Name the updater's argument after the state variable it updates. Python has no single-letter convention as strong as JavaScript's, but the intent is the same — the argument is the previous value:

set_enabled(lambda enabled: not enabled)
set_last_name(lambda last_name: last_name[::-1])
set_friend_count(lambda friend_count: friend_count * 2)

For clarity, prefer a descriptive name over n: lambda previous: previous + 1 reads better in a handler that also does other work.

Recap

  • Setting state does not change the variable in the existing render; it requests a new render.
  • ReactPy processes state updates after a synchronous event handler has finished. That is batching.
  • Batching does not survive an await inside an async def handler — each await can flush a render with partial updates.
  • To update the same state several times in one handler, use updater functions: set_number(lambda n: n + 1).
  • A plain value replaces the state and discards earlier updater results; an updater is applied to the current value.
  • Updater functions must be pure, and ReactPy will not check them for you.
Authors: Archmonger