Skip to content

Set a controlled widget back after an event that caused a render - #62

Closed
maartenbreddels wants to merge 1 commit into
masterfrom
fix/fast-event-writeback
Closed

maartenbreddels wants to merge 1 commit into
masterfrom
fix/fast-event-writeback

Conversation

@maartenbreddels

@maartenbreddels maartenbreddels commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Why

Take a controlled widget, where the element gives the value and a handler maps the new value to state:

value, set_value = use_state("AA")
def on_value(v):
    set_value(v.upper())
    set_count(lambda n: n + 1)  # some other state, e.g. in a sibling component
w.Text(value=value, on_value=on_value)

Type "Bb": the handler sets "BB", which is the state we already have, and it changes the count, so a render happens.

  • The default renderer walks the whole tree in that render and sets every widget back to its element's values. The text shows "BB".
  • The fast renderer (REACTON_FAST=1) skips the component that did not change, so the text keeps showing "Bb" while the state is "BB".

This is a case of vuejs/vue#13237.

handler default, master fast, master this PR, both
a sibling's state changes too (render) "BB" "Bb" ✗ "BB"
no other state change (no render) "Bb" "Bb" "Bb" (unchanged)

What

The event listener is now a small class, _TraitListener, that remembers the element that currently owns the widget. That element is updated when a new element keeps the same callback. After a handler that caused a render, the listener sets the trait back to that element's value. So the fast renderer now gives the default renderer's result.

  • If the render replaced the listener (new callback), it does nothing: the new element has already been applied.
  • If the element gives no value (uncontrolled), the widget keeps what the user entered.
  • It also does nothing during a render, when the render closed the widget, or when the value holds elements.

When nothing rendered, nothing changes, in both renderers. A first version always set the widget back, like React and like #45. That broke 5 of solara's input_test.py tests: InputInt/InputFloat with continuous_update=False pass the old value, ignore v_model changes while you type, and take the new value on blur, so every keystroke snapped back. Fixing the no-render case needs a change in solara first.

Tests

In both renderers:

  • test_controlled_widget_no_state_change_sibling_renders[layout] and test_controlled_widget_no_state_change_parent_renders: fail on master in the fast renderer, pass in the default renderer.
  • test_controlled_widget_same_callback_new_value: the callback is the same object on every render, so the value set back must come from the new element.
  • test_controlled_widget_no_render_keeps_user_value: pins the solara continuous_update=False behavior.
  • test_uncontrolled_widget_keeps_its_value, test_controlled_widget_removed_by_handler: guard tests.
  • Solara's unit suite: the same results as master, with REACTON_FAST=0 and =1.

🤖 Generated with Claude Code

When the user changes a widget whose element gives the value (value=X,
on_value=h), and h changes state so that a render happens, the default
renderer sets every widget in the tree back to its element's values,
because it walks the whole tree. The fast renderer skips components
that did not change. So when h leaves its own state as it is (it
upper-cases "Bb" to the "BB" we already have) but changes other state,
the widget kept showing "Bb" while the state was "BB", only in the fast
renderer. That is a case of vuejs/vue#13237.

The event listener now remembers the element that currently owns the
widget, and after a handler that caused a render, it sets the trait
back to that element's value. That gives the fast renderer the result
of the default renderer. A listener that the render replaced does
nothing, since the new element has already been applied.

When nothing rendered, the widget keeps what the user entered, in both
renderers, as before. A first version always set it back (like React,
and like #45), but solara's inputs with
continuous_update=False depend on this: they pass the old value, ignore
v_model changes, and take the new value on blur. With a write-back,
typing snapped back on every key.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@maartenbreddels maartenbreddels changed the title Set a controlled widget back to its element's value after an event Set a controlled widget back after an event that caused a render Sep 28, 2026
@maartenbreddels

Copy link
Copy Markdown
Contributor Author

not worth a performance hit maybe?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant