solara icon indicating copy to clipboard operation
solara copied to clipboard

Batch update: automatically?

Open maartenbreddels opened this issue 1 year ago • 4 comments

Current situation

Currently, doing two state changes in a callback, will result in two render passes. If rendering is seen as a side-effect, rendering should only happen after the event handler is finished.

The workaround to get batch updates in event handlers for now is: (not a public solara API, might break in the future):

import solara
import reacton.core


text = solara.reactive("Initial text")

@solara.component
def Page():
    rc = reacton.core.get_render_context()
    print("Render with: ", text.value)
    def change_text():
        with rc:
            text.value = "Should never be rendered visible"
            text.value = "Updated text"
        
    solara.Button("Change text", on_click=change_text)
    solara.Text(text.value)

This has the upside of being explicit and allowing people to opt out (by simply not doing this).

Making this the default in Solara 2.0?

If we make batch updates in event handlers the default behaviour, it would be difficult to opt out (I cannot think of a good name or a way to signal this as a user).

Better API in 2.0?

Instead of making it a default and not easy to use (you have to get a handle to the render-context from the main render loop), we can think of a solara.batch_update() function/context-manager that more explicitly conveys intent.

import solara


text = solara.reactive("Initial text")

@solara.component
def Page():
    print("Render with: ", text.value)
    def change_text():
        with solara.batch_update:
            text.value = "Should never be rendered visible"
            text.value = "Updated text"
        
    solara.Button("Change text", on_click=change_text)
    solara.Text(text.value)

Consistency with tasks

If callbacks do automatic batch updating, people might expect a task to do the same. Should we even allow people to update reactive variables from a task, or should we only allow a return value (force them to be pure)? The progress value would then be an exception to this, since that is typically updated in a loop:

    import time
    import solara
    from solara.lab import task


    @task
    def my_calculation():
        total = 0
        for i in range(10):
            # mutate progress in the task
            my_calculation.progress = (i + 1) * 10.0
            time.sleep(0.4)
            if not my_calculation.is_current():
                # A new call was made before this call was finished
                return
            total += i**2
        return total


    @solara.component
    def Page():
        solara.Button("Run calculation", on_click=my_calculation)
        solara.ProgressLinear(my_calculation.progress if my_calculation.pending else False)

        if my_calculation.finished:
            solara.Text(f"Calculation result: {my_calculation.value}")
        elif my_calculation.not_called:
            solara.Text("Click the button to fetch data")

maartenbreddels avatar May 08 '24 15:05 maartenbreddels

This batch_update feature would be a game change. A massive upvote for this.

JovanVeljanoski avatar May 15 '24 13:05 JovanVeljanoski

from The Zen of Python: "Explicit is better than implicit." Which would argue in favor of an explicit batch_update. That being set, setting .value on a reactive also involves automagic, but there I think its more explicitly expected that settings a reactive value is no ordinary setattr operation.

I think if you make this default / automatic in callbacks it might be confusing as users could expect multiple renders from setting multiple reactive values.

wrt tasks, I do at the moment set reactives in the global scope from tasks, so that would break my use case but if things become clearer (or glitch-free, no illegal states/incompatible combination of interdependent reactive values) then I wouldn't mind refactoring that

Jhsmit avatar May 22 '24 09:05 Jhsmit

We could make batch updating the default in Solara 2.0, and allow disabling it with an environmental variable (like SOLARA_BATCH_UPDATE=0) together with a context manager to enable it in places when disabled elsewhere. However, I guess this approach would be the most work to implement.

In general I'd be in favour of batch updating by default, since this would improve both consistency in our reactivity system and performance. I also think it covers the overwhelming majority of use cases. The behaviour of tasks is indeed tricky. I think batching all updates from within a task doesn't make sense, but at the same time there isn't really a way for us to distinguish what updates from within a task should and shouldn't be batched.

iisakkirotko avatar Dec 16 '24 09:12 iisakkirotko

This is nice. However, it appears to interfere with some component behavior. For example, I have a dynamic tabs component (add/remove tabs). On add/remove, I fire a .set() operation on two reactive variables. I used the approach suggested here to batch the events, which works. But it also interrupts the smooth animation of newly added tabs (enter from right of screen). Note that once in place, the animation works on tab change. But the initial animation on adding the new tab content to the DOM does not work.

edan-bainglass avatar May 29 '25 09:05 edan-bainglass