2
votes

I work on the react-native application using Clojurescript re-frame and reagent. I have one text input component, and have two versions of the code:

Version 1: input text is a separate component, and state atom is passed as an argument, the same as the recommended in the reagent library docs and examples.

(defn todo-input [value]
  [rn/text-input
   {:style          (styles :textInput) :multiline true
    :placeholder    "What do you want to do today?" :placeholder-text-color "#abbabb"
    :value          @value
    :on-change-text #(reset! value %)}]
  )

(defn todo-screen []
  (let [value (r/atom nil)]
    [rn/view {:style (styles :container)}
     [rn/text {:style (styles :header)} "Todo List"]
     [rn/view {:style (styles :textInputContainer)}
      [todo-input value]
      [rn/touchable-opacity
       {:on-press (fn [] (rf/dispatch [:add-todo @value]) (reset! value nil))}
       [icon {:name "plus" :size 30 :color "blue" :style {:margin-left 15}}]]]
     [todos]
     ]))

Version 2: everything in one component.

(defn todo-screen []
  (let [value (r/atom nil)]
    [rn/view {:style (styles :container)}
     [rn/text {:style (styles :header)} "Todo List"]
     [rn/view {:style (styles :textInputContainer)}
      [rn/text-input
       {:style          (styles :textInput) :multiline true
        :placeholder    "What do you want to do today?" :placeholder-text-color "#abbabb"
        :value          @value
        :on-change-text #(reset! value %)}]
      [rn/touchable-opacity
       {:on-press (fn [] (rf/dispatch [:add-todo @value]) (reset! value nil))}
       [icon {:name "plus" :size 30 :color "blue" :style {:margin-left 15}}]
       ]]
     [todos]]))

The issue is that first version has a performance issue while typing, since there's a big delay and flickering when trying to type fast. Version 2 doesn't have any issues, and I can type as fast as I can.

According to the reagent documentation, passing r/atom as a parameter should not incur any performance issues.

Am I doing something wrong here? What would be the best way to avoid performance penalty here.

This is a small example, and having one component instead of two is not a big deal, but splitting one big to multiple smaller components in a good praxis. State here is local to the component, and I don't want to use re-frame for it.

3

3 Answers

1
votes

re-frame/dispatch puts your events in a queue for re-frame to process, so there can be a slightly delay before it actually goes through and your change will be there.

Sounds like you're experiencing the same issue stated here: https://github.com/day8/re-frame/issues/368

So one work around is to use re-frame.core/dispatch-sync which forces re-frame to handle the event directly and synchronously. You might have to also add a call to reagent.core/flush to force re-render the component. I haven't needed flush before when building web clients, but React Native seems to work differently.

You can read more about these two functions here:

Mentioned in the issue above is also https://github.com/Day8/re-com that supposedly works around the issue somehow, but I didn't take a closer look at that.

Your solution #2 is not wrong either, it just gives you a different way of working. So if you need the data in your app-db to update on every keypress for example, only something more like #1 will work. Or using solution #2 but passing in the atom as an argument to your component.

1
votes

Both of your versions have an issue. You should use Form-2 type components when using local state. Like this:

(defn todo-screen []
  (let [value (r/atom nil)]
    (fn []
      [rn/view {:style (styles :container)}
       [rn/text {:style (styles :header)} "Todo List"]
       [rn/view {:style (styles :textInputContainer)}
        [todo-input value]
        [rn/touchable-opacity
         {:on-press (fn [] (rf/dispatch [:add-todo @value]) (reset! value nil))}
         [icon {:name "plus" :size 30 :color "blue" :style {:margin-left 15}}]]]
       [todos]])))

More info about Form-2 here.

Or you could use the r/with-let instead. More info about with-let.

Regarding your original question, you could have a compromise between both your versions, and extract the input and submit button into a separate component:

(defn todo-input-container [on-press]
  (r/with-let [value (r/atom nil)]
    [rn/view {:style (styles :textInputContainer)}
     [rn/text-input
      {:style          (styles :textInput) :multiline true
       :placeholder    "What do you want to do today?" :placeholder-text-color "#abbabb"
       :value          @value
       :on-change-text #(reset! value %)}]

     [rn/touchable-opacity
      {:on-press (fn []
                   (on-press @value)
                   (reset! value nil))}
      [icon {:name "plus" :size 30 :color "blue" :style {:margin-left 15}}]]]))


(defn todo-screen []
  [rn/view {:style (styles :container)}
   [rn/text {:style (styles :header)} "Todo List"]
   [todo-input-container (fn [value] (rf/dispatch [:add-todo value]))]
   [todos]])

0
votes

Looks like the issue here is that reagent doesn't support well components with the controlled inputs due to its async nature.

Controlled input (via :value) should be avoided, or worked around by forcing component update immediately after changing :value.

See reagent issue and explanation for more details.