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]])))
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]])