reagent-project/reagent

Controlled input loses cursor under ShadowRoot in React 18

kimo-k opened this issue · 3 comments

kimo-k commented

This controlled-input cursor bug appears when I try to use the new react.dom.client API to render an :input component within a shadow root. The text cursor skips to the end every time I insert a character.

The same component works fine with the react.dom API.

Is this expected, and is there anything I can do? I'm reticent to go with the react-native workaround, since my codebase is pretty large, with everything inside a shadow root (i.e. re-frame-10x).

Minimal repro: master...kimo-k:reagent:shadow-root-controlled-input-bug

Not sure if this fixes the issue but that code doesn't seem quite right.

It's creating a new input element every render cycle instead of updating an existing one. You could try adding a :key property to see if that improves the tracking. When I've ran into similar issues, it was usually because it was deleting the previous dom element and replacing it with a new one instead of updating it.

Lastly it seems like it's better to use with-let to initialize your atom in the repro component definition so you don't have to return a function. That way the atom is created only once in the component's lifecycle.

(defn repro []
  (with-let [text (r/atom "")]
    [:input {:on-change #(reset! text (.. % -target -value))
             :value @text}]))

Not sure that will entirely fix it, but it's a place to start.

Hey @jaidetree, thanks for taking a look. Were you able to run the repro without bugs?

I think your hypothesis might be incorrect, though. As far as I know, repro is an idiomatic form-2 component, :key would have no effect outside a seq, and using with-let will macroexpand to the same code.

@kimo-k Just note about :key. It isn't specific to sequences at all, any React component can have a key and React will use it to detect if the component identity changes. If identity changes, the previous component is unmounted and a new one created. This is different to just updating properties for same component instance. This can be useful outside of seqs when you need to reinitialize component local state (ratoms or hook state).

with-let implementation also does other stuff than just macroexpand into a basic form-2 component. Though it does indeed wrap the with-let body into (fn [] ...), i.e. expand into form-2 component.

Reagents controlled input cursor hacks are a huge mess and our async rendering batching for ratom changes is really problematic with React 18 update batching so there is probably no easy fix for this.