Skip to content

model.useState boomerang causes character loss in React ESM text inputs #8523

@MarcSkovMadsen

Description

@MarcSkovMadsen

Description

When typing fast in any ReactComponent that uses model.useState() for a controlled text input, characters are dropped. This is the React ESM equivalent of the boomerang issue fixed for Bokeh components in PR #7093.

Here is an example from my deployed panel-material-ui ChatInterface application.

panel-material-ui-boomerang.mp4

I typed "boomerang. Sometimes". But it became "boomerang. Sometims". I'm normally only able to reproduce for deployed applications. While developing on my jupyterhub I don't see the issue.

AI Disclaimer

Below is AI Generated. I've

Minimal Reproducible Example

import param
import panel as pn

from panel.custom import ReactComponent

pn.extension()


class TypingTest(ReactComponent):
    value_input = param.String(default="")

    _esm = """
    export function render({model}) {
      const [value_input, setValueInput] = model.useState("value_input")
      return (
        <div>
          <textarea
            value={value_input}
            onChange={(e) => setValueInput(e.target.value)}
          />
          <p>value_input: {value_input}</p>
          <p>length: {value_input.length}</p>
        </div>
      )
    }
    """

TypingTest().servable()

Run with panel serve script.py and type fast — characters will be dropped.

Root Cause

model.useState in panel/models/react_component.ts (lines 497-540) creates a bidirectional sync with no boomerang prevention:

  • Effect 1 (Python → React): calls setValue(targetModel.attributes[resolvedProp]) on any property change
  • Effect 2 (React → Python): calls targetModel.setv({prop: value}) when React state changes

The race condition:

  1. User types "a" → setValueInput("a") → Effect 2 sends "a" to Python
  2. User types "b" → setValueInput("ab") → Effect 2 sends "ab" to Python
  3. Python echoes "a" back → Effect 1 fires setValue("a")overwrites "ab"
  4. Character "b" is lost

Real-world Impact

This affects panel-material-ui ChatInterface and all text input widgets (TextInput, TextAreaInput, PasswordInput, AutocompleteInput) — any component using the model.useState + controlled input pattern.

Precedent

This is the same class of bug fixed for Bokeh components in:

Proposed Fix

Add boomerang detection to model.useState in react_component.ts. Track values sent to Python via a ref array. When the Python → React callback fires, check if the incoming value is an echo of a locally-sent value — if so, skip the setValue() call.

const sentRef = React.useRef([])

// Effect 1: Python → React — skip echoes
React.useEffect(() => {
  const cb = () => {
    if (target.model.events.includes(resolvedProp)) {
      // ... event handling unchanged ...
    } else {
      const incoming = targetModel.attributes[resolvedProp]
      const idx = sentRef.current.indexOf(incoming)
      if (idx !== -1) {
        sentRef.current.splice(0, idx + 1)  // Echo — discard
      } else {
        sentRef.current.length = 0
        setValue(incoming)  // Genuine server update — apply
      }
    }
  }
  react_proxy.on(prop, cb, true)
  return () => react_proxy.off(prop, cb)
}, [])

// Effect 2: React → Python — record sent values
React.useEffect(() => {
  if (!target.model.events.includes(resolvedProp)) {
    sentRef.current.push(value)
    targetModel.setv({ [resolvedProp]: value })
  }
}, [value])

Environment

  • Panel: 1.8.10 (but affects all versions with React ESM support)
  • Python: 3.12
  • Browser: Chromium (all browsers affected)

Metadata

Metadata

Assignees

No one assigned

    Labels

    TRIAGEDefault label for untriaged issues

    Type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions