Fork me on GitHub
#reagent
<
2021-08-15
>
Johnny12:08:26

Hey there, I'm new to both React and reagent and I'm looking for a way to prevent a component from re-rendering while it is in focus. What is the recommended way to do something like that?

p-himik13:08:09

It should be possible to specify this function using reagent.core/create-class: https://reactjs.org/docs/react-component.html#shouldcomponentupdate

p-himik13:08:29

So it would always return false when something in the component is in focus. But TBH, what you're describing something rather suspicious. My intuition is that it would be better to track "component is focused" as a part of your app's state and make explicit decision at the component's parent level whether to send in the new props or not, or something like that.

Johnny13:08:26

Initially, I wanted to do that, but I couldn't get it to work. The function was simply never called. Then, I read that this should only be used for performance optimisations, so I thought I'd ask elsewhere first to see if there's a better way and before I waste my time on this.

Johnny13:08:53

Hmm, trying to do this via the parent seems like a good idea. I'm not sure if this'll work in my case, but I can try

Johnny13:08:04

For context, what I have is essentially an editor for some structured data, and multiple components that represent different ways to edit it (and they should all be synced with the data). However I don't want to update the text editor component while a user is typing because that leads to weird trip-ups

p-himik13:08:28

Ah! Then just change the relevant data only when the focus is lost!

p-himik13:08:54

Keep the internal state in an internal ratom, update the external one once the component loses focus or a user presses Enter.

Johnny13:08:01

I want the other components to update while typing though

Johnny13:08:19

Also, thinking about using props and deciding it in the parent, it would still be re-rendered either way, since I can't just say "use the component you had before", right?

p-himik13:08:36

Oh, so you mean that if you enter something in a text area, you don't want that very same text area to receive the newly entered data because it messes with up the text cursor?

Johnny13:08:11

Right, yeah, and this editor component also auto-formats which doesn't really work live

Johnny13:08:44

Or like, when someone is typing and the value gets replaced simultaneously

p-himik13:08:56

Alright. What I describe specifically is a problem of text input components in Reagent. Or, rather, in React in general if the component is a controlled one. Some libraries fix it, some don't. Reagent fixes it for the regular HTML inputs, but e.g. for a Material-UI text input I had to fix it myself by IIRC wrapping an uncontrolled component in a thin controlled wrapper. But I think you describe a different scenario. I think having two sets of ratoms would still solve it - you would dereference either the internal ratom or the external one, depending on whether the component is focused or not.

Johnny13:08:35

As I said, I don't really want to update only when focus is lost.

p-himik13:08:20

You can organize that with an appropriate ratom separation. I'm afraid your problem description is still too vague for me to write something more specific.

Johnny13:08:35

Well I had been thinking about each editor component having its own view of the data but I thought this would be really bad in terms of design because of redundancy and consistency. Also as soon as you introduce more ways to edit the data you run into the mediator problem

Johnny13:08:45

I'm sorry if I'm being vague, as I said I'm really new to this. I've never done proper frontend development before

Johnny13:08:21

This is the renderer for the component in question, if it helps at all. commands is the shared data

(fn [format]
  [:> ace-editor
   {:mode (ace-mode format)
    :width ""
    :height ""
    :class "min-h-full rounded border-2 border-gray-400 border-b-1 w-full"
    :value (con/write-str format @commands)
    :on-change (fn [text _] (reset! commands (con/read-str format text)))
    :on-blur (fn [_ _] (reset! focused false))
    :on-focus (fn [_] (reset! focused true))}])

Johnny13:08:02

Ignore the on-blur and on-focus handlers, these were from my previous attempts

p-himik13:08:52

That's alright. If you still think that there's something good in that idea, you should come up with the simplest example that would demonstrate it, and then write out the desired scenario. Something like "there are two text inputs on a page; the user types a sentence in the first field - the second field gets updated with the result of a function called on the contents of the first input; user types something in the second input field - something else happens". I'm afraid your code doesn't help me at all because I still have no idea what the usage scenario is. What the, possibly imaginary, state machine is - what the states are, what the transitions are.

Johnny14:08:21

All editor components share the same data, they just display it differently and allow you to edit it differently. To the left there is a visual representation of the data that can be edited through a GUI. To the right there is the textual representation of the data which can be edited like in a text editor. When I add or change something in either, the other is updated as well.

Johnny14:08:03

That's really all there is to it

p-himik14:08:35

> weird trip-ups > this editor component also auto-formats which doesn't really work live > when someone is typing and the value gets replaced simultaneously So far it all sounds like it's a fundamental problem with that ace-editor and not with how you use it.

p-himik14:08:13

Or maybe it's your (con/write-str format ...) - if so, then just don't use that function until the editor is out of focus.

Johnny14:08:31

I don't think this is an issue with specifically this editor. This is an inherent issue if you replace the content while typing and reset the cursor position

Johnny14:08:05

The problem is that changes - even though they are already reflected in the editor - cause the component to re-render.

Johnny14:08:30

> Or maybe it's your (con/write-str format ...) - if so, then just don't use that function until the editor is out of focus. Also this sounds great but it will still be re-rendered regardless if I deref the data atom anywhere in the renderer + I would need to keep track of the entire editor's state and restore it every time an unnecessary update is triggered. This just seems really bad.

Johnny14:08:34

I think my best bet is probably still shouldComponentRender(). So I'll just ask about that instead:

(defn test-comp []
  (r/create-class
   {:display-name "test-comp"
    :reagent-render (fn [] [:strong (pr-str @commands)])
    :should-component-update (fn [_ _ _] (println "NO IT SHOULDNT") false)}))
I have this component as a test and I'm wondering what's wrong with it, because the :should-component-update function never seems to be called and it always re-renders.

p-himik14:08:30

> it will still be re-rendered What is the exact issue with just re-rendering itself? > I would need to keep track of the entire editor's state and restore it every time an unnecessary update is triggered. This just seems really bad. Not the entire state - just the one you feed it from the outside.

Johnny14:08:39

> What is the exact issue with just re-rendering itself? Redundancy + auto-formatting

Johnny14:08:10

Also I'd have to store other state that I shouldn't have to care about like cursor position

p-himik14:08:28

Not sure why :should-component-update isn't called, but in Reagent source code it's conditioned on reagent.impl.util/*always-update*.

Johnny14:08:59

Should I disable that or...? I feel like this should be documented, no?

Johnny14:08:44

Setting it to false doesn't seem to have an effect anyway

p-himik15:08:02

No clue whatsoever, sorry.

Johnny15:08:18

Hmm. I might make a minimal app that uses that lifecycle method and even open a GitHub issue if it doesn't work at all

Johnny15:08:58

The documentation on this needs to be updated anyway, they call this method component-should-update in the tutorial

lilactown15:08:35

how often does the value the editor shows change when it's not focused?

lilactown15:08:41

I was working on a problem just like this a couple days ago

lilactown15:08:38

what I did is have the editor component be "uncontrolled" - i.e. it doesn't have its :value controlled by the ratom

lilactown15:08:21

and then specific actions would force the editor component to remount

lilactown15:08:41

here's sort of what it looked like:

(r/with-let [editor-inst (r/atom 0)
             editor-value (r/atom "")]
  [:div
   [editor {:key @editor-inst
            :initial-value @editor-value
            :on-change #(reset! editor-value %)}]
   [:button #{:on-click (fn []
                          (reset! editor-value "")
                          (swap! editor-inst inc))}
    "Clear"]])

lilactown15:08:01

so the editor would ignore any changes to the value of editor-value , whether it was made by the editor itself or outside. but if you click the "Clear" button, it would change the editor-value and force the editor component to reinitialize with the new value by changing the value passed to its :key prop

lilactown15:08:52

:key is used by React to determine whether it should maintain the same component instance between renders, you can use this strategy with any component or element to force a remount

Johnny15:08:48

This is an interesting approach, hmm

Johnny15:08:50

What still doesn't sit right with me is the additional coupling this introduces. Idk if that's just me

Johnny15:08:29

What I mean is, like, these components should be independent. The only thing they should share and care about is the data atom. It's a bit frustrating to me that there seems to be no "pretty" way to achieve this

lilactown15:08:15

handling text input is tricky because there's so much essential complexity

lilactown15:08:43

most elements on the page we can just blow away and redraw, but text inputs & editors have a ton of internal state - cursor position, text selection, etc.

Johnny15:08:03

that's fair, yeah.

Johnny15:08:18

Is there any chance you know anything about should-component-update?

lilactown15:08:21

forms are another place where this "let the DOM take the wheel" approach works better IME

Johnny15:08:27

Trying to weigh my options here

lilactown15:08:59

let me see... just to make sure, what version of reagent are you using?

lilactown16:08:34

I'm seeing similar behavior... it's not calling the fn I pass to :should-component-update when I change a ratom

lilactown16:08:41

I do see it called when you pass in new props

lilactown16:08:54

here's a working example:

(defn test-comp
  [_]
  (r/create-class
   {:display-name "test-comp"
    :reagent-render (fn [commands] [:strong (pr-str commands)])
    :should-component-update (fn [_ _ _] (println "NO IT SHOULDNT") false)}))


(defn app
  []
  [test-comp @commands])

lilactown16:08:53

this actually matches my expectations from pure React. shouldComponentUpdate is only called when new props are passed in, not when internal state changes

lilactown16:08:57

the confusing part is that in this case it's not internal state changing but an external reagent atom. but Reagent updates the component by calling an internal setState, which is why it bypasses shouldComponentUpdate

Johnny08:08:04

That makes sense, yeah