Multiple State Updates#

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 React batches state updates.

Batched Updates#

As we learned previously, state variables remain fixed inside each render as if state were a snapshot taken at the beginning of each render. This is why, in the example below, even though it might seem like clicking the “Increment” button would cause the number to increase by 3, it only does by 1:

from reactpy import component, html, run, use_state


@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({"on_click": handle_click}, "Increment"),
    )


run(Counter)

The reason this happens is because, so long as the event handler is synchronous (i.e. the event handler is not an async function), ReactPy waits until all the code in an event handler has run before processing state and starting the next render. Thus, it’s the last call to a given state setter that matters. In the example below, even though we set the color of the button to "orange" and then "pink" before "blue", the color does not quickly flash orange and pink before blue - it always remains blue:

from reactpy import component, html, run, use_state


@component
def ColorButton():
    color, set_color = use_state("gray")

    def handle_click(event):
        set_color("orange")
        set_color("pink")
        set_color("blue")

    def handle_reset(event):
        set_color("gray")

    return html.div(
        html.button(
            {"on_click": handle_click, "style": {"background_color": color}},
            "Set Color",
        ),
        html.button(
            {"on_click": handle_reset, "style": {"background_color": color}}, "Reset"
        ),
    )


run(ColorButton)

This behavior let’s you make multiple state changes without triggering unnecessary renders or renders with inconsistent state where only some of the variables have been updated. With that said, it also means that the UI won’t change until after synchronous handlers have finished running.

Note

For asynchronous event handlers, ReactPy will not render until you await something. As we saw in prior examples, if you introduce an asynchronous delay to an event handler after changing state, renders may take place before the remainder of the event handler completes. However, state variables within handlers, even async ones, always remains static.

This behavior of ReactPy to “batch” state changes that take place inside a single event handler, do not extend across event handlers. In other words, distinct events will always produce distinct renders. To give an example, if clicking a button increments a counter by one, no matter how fast the user clicks, the view will never jump from 1 to 3 - it will always display 1, then 2, and then 3.

Incremental Updates#

While it’s uncommon, you need to update a state variable more than once before the next render. In these cases, instead of having updates batched, you instead want them to be applied incrementally. That is, the next update can be made to depend on the prior one. For example, what it we wanted to make it so that, in our Counter example from before, each call to set_number did in fact increment number by one causing the view to display 0, then 3, then 6, and so on?

To accomplish this, instead of passing the next state value as in set_number(number + 1), we may pass an “updater function” to set_number that computes the next state based on the previous state. This would look like set_number(lambda number: number + 1). In other words we need a function of the form:

def compute_new_state(old_state):
    ...
    return new_state

In our case, new_state = old_state + 1. So we might define:

def increment(old_number):
    new_number = old_number + 1
    return new_number

Which we can use to replace set_number(number + 1) with set_number(increment):

from reactpy import component, html, run, use_state


def increment(old_number):
    new_number = old_number + 1
    return new_number


@component
def Counter():
    number, set_number = use_state(0)

    def handle_click(event):
        set_number(increment)
        set_number(increment)
        set_number(increment)

    return html.div(
        html.h1(number),
        html.button({"on_click": handle_click}, "Increment"),
    )


run(Counter)

The way to think about how ReactPy runs though this series of set_state(increment) calls is to imagine that each one updates the internally managed state with its return value, then that return value is being passed to the next updater function. Ultimately, this is functionally equivalent to the following:

set_number(increment(increment(increment(number))))

So why might you want to do this? Why not just compute set_number(number + 3) from the start? The easiest way to explain the use case is with an example. Imagine that we introduced a delay before set_number(number + 1). What would happen if we clicked the “Increment” button more than once before the delay in the first triggered event completed?

import asyncio

from reactpy import component, html, run, use_state


@component
def Counter():
    number, set_number = use_state(0)

    async def handle_click(event):
        await asyncio.sleep(3)
        set_number(number + 1)

    return html.div(
        html.h1(number),
        html.button({"on_click": handle_click}, "Increment"),
    )


run(Counter)

From an earlier lesson, we learned that introducing delays do not change the fact that state variables do not change until the next render. As a result, despite clicking many times before the delay completes, the number only increments by one. To solve this we can use updater functions:

import asyncio

from reactpy import component, html, run, use_state


@component
def Counter():
    number, set_number = use_state(0)

    async def handle_click(event):
        await asyncio.sleep(3)
        set_number(lambda old_number: old_number + 1)

    return html.div(
        html.h1(number),
        html.button({"on_click": handle_click}, "Increment"),
    )


run(Counter)

Now when you click the “Increment” button, each click, though delayed, corresponds to number being increased. This is because the old_number in the updater function uses the value which was assigned by the last call to set_number rather than relying in the static number state variable.