reactpy icon indicating copy to clipboard operation
reactpy copied to clipboard

Client-side component events break input focus

Open Archmonger opened this issue 1 year ago • 4 comments
trafficstars

Discussed in https://github.com/reactive-python/reactpy/discussions/1187

Originally posted by williamneto January 13, 2024 So, while developing reactpy-material i faced a problem that might be related to the reactpy architecture for custom components.

To showcase the problem i created this small component, just a normal textfield. The problem is when attaching a on_change event to this custom component, the event on the page doesn't behavior as expected. Each time you type in the field (and trigger the onChange event) the page focus is moved out of the input element.

So, a simple custom TextField like that

import React from "react";
import ReactDOM from "react-dom";
import htm from "htm";

const html = htm.bind(React.createElement);

export function bind(node, config) {
  return {
    create: (type, props, children) => React.createElement(type, props, ...children),
    render: (element) => ReactDOM.render(element, node),
    unmount: () => ReactDOM.unmountComponentAtNode(node),
  }
}

export function TextField(attrs) {
  return html`
    <div>
      <input type="text" ...${attrs}>
    </div>`;
}
from pathlib import Path
from typing import Any
from reactpy.web.module import export, module_from_file
from reactpy import component

_js_module = module_from_file(
    "reactpy-events-problem",
    file=Path(__file__).parents[0] / "bundle.js"
)

new_text_field = export(_js_module, "TextField")

@component
def text_field(attrs: Any ={}):
    return new_text_field(attrs)

And when you try to use it like that

from reactpy import component, run, html, use_state
from reactpy_events_problem import text_field

@component
def app():
    text, set_text = use_state("Text")

    return html.div(
        html.h1(text),
        text_field(
            attrs={
                "value": text,
                "onChange": lambda e: set_text(e["target"]["value"])
            }
        )
    )

run(app)

You end up with this weird behavior at the page focus, making type in this field very unpratical

Archmonger avatar Feb 11 '24 05:02 Archmonger

Relevant code is here. What's probably happening is that the element is getting recreated.

With that said, despite the fact that render() on the custom component is always called, subsequent calls to ReactDOM.render on the same element should trigger an update rather than a render from scratch.

One possible cause is that the element React is mounting to is changing for some reason.

rmorshea avatar Feb 11 '24 20:02 rmorshea

On a separate note, we might want to add a method to custom components (perhaps mount()) that's called exactly once when the custom component is first rendered to a given DOM element. This would match up with the new createRoot(...).render() workflow in ReactJS 18.

rmorshea avatar Feb 11 '24 20:02 rmorshea

I have confirmed this same exact behavior using a custom FormControl component from react-boostrap. I have also noticed what is likely the same root issue in a less annoying case using a custom Map component from pigeon-maps. In this latter case, the map flickers every time a change is triggered as if the image tiles are being re-rendered every time. This issue is a big deal based on what I hope to accomplish with reactpy and reactpy-django. So much so that I'd love to be part of the solution. I'm going to take a stab at detangling this, but this still all feels very black-boxy to me. Let me know if anyone else has made progress here before I waste too much time. Thanks!

shawncrawley avatar Jul 17 '24 18:07 shawncrawley

I don't believe anyone has had time to untangle this issue yet, so feel free to take a stab at it!

Archmonger avatar Jul 17 '24 18:07 Archmonger