Target the right element when a set_focus/set_value id is built from a Var

FA
4h ago
+194-17

All Submissions:

  • Have you followed the guidelines stated in CONTRIBUTING.md file?
  • Have you checked to ensure there aren't any other open Pull Requests for the desired changed?

Type of change

  • Bug fix (non-breaking change which fixes an issue)

Changes To Core Features:

  • Have you added an explanation of what your changes do and why you'd like us to include them?
  • Have you written new tests for your core changes, as applicable?
  • Have you successfully ran tests with your changes locally?

Problem

rx.foreach(
    S.items,
    lambda item: rx.hstack(
        rx.input(id=f"rf_{item}"),
        rx.button("focus", on_click=rx.set_focus(f"rf_{item}")),
        rx.button("fill", on_click=rx.set_value(f"rf_{item}", item)),
    ),
)

Clicking "focus" or "fill" on the first row acted on the last row's input. rx.set_focus, rx.blur_focus and rx.set_value format the id into a ref name at compile time and drop the Var marker of the f-string, so every row sent ref: "ref_rf_item_rx_state_", a name the last rendered row had registered. Reproduced on main.

Fix

An id built from Vars is only known when the event runs, so it cannot name a ref chosen at compile time.

  • Python (reflex_base/event/__init__.py): for an id that is a Var or an f-string holding one, the three events send the id as it evaluates, in its own payload field (id: ("rf_"+item_rx_state_)). A literal id is unchanged and still sends its registered ref name (ref: "ref_input1").
  • Frontend (utils/state.js): _set_focus, _blur_focus and _set_value resolve ref through the refs registry exactly as on main, and resolve id with document.getElementById only. The two never cross, so a row id that happens to evaluate to ref_alpha, or to a name every object inherits such as toString, cannot be taken for a registered ref.

This is a different route from the one #7838 sketches (registering each row's ref under a key derived from its evaluated id). That needs a hook whose argument reads the loop variable, which is only safe where the hook is guaranteed to compile inside the loop; it is not in an @rx.memo body (#7836) and not for a component the per-item pass leaves unwrapped. Looking the element up by id needs no hook and works in every one of those places. rx.scroll_to already resolves its target with document.getElementById.

As a side effect the three events now accept a plain Var id (rx.set_focus(State.target_id)), which raised a TypeError before.

Not changed

  • A component with an f-string id still registers its (shared, unused) static ref; removing it would change what Form collects for such controls, so I left it alone.
  • A backend handler that returns rx.set_focus(f"rf_{self.selected}") passes a plain string, which is formatted to ref_rf_alpha as before and still finds nothing for an input whose id came from a Var. Supporting that needs the raw id in the payload for literal ids too, which changes the payload of every existing use, so I kept it out of this fix. Happy to follow up if you want it.

Tests

  • tests/units/test_event.py: the exact event the three functions compile to for an f-string id and for a plain Var id.
  • packages/reflex-frontend/tests/state.test.js: ref finds a registered ref by name, id finds an element by its id (including ids spelled ref_named and toString), and a missing target still logs the "No element found" error. Fails without the state.js change.
  • tests/integration/tests_playwright/test_server_side_event.py: the existing app gets an rx.foreach whose rows fill and focus their own input. Without the fix the first row's input stays empty.

Ran locally: tests/units (minus reflex_bench, whose browser test cannot start Chromium in my sandbox), npm run check in packages/reflex-frontend (lint, type-check, vitest), the Playwright modules test_server_side_event and test_input, plus ruff check, ruff format and pyright on the changed files.

closes #7838

https://claude.ai/code/session_01LwJUCTCgM7D4Vj6H4dRWss