Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pattern for Displaying Errors To UI In Reagent/Clojurescript

I often find myself doing things like this when working with Reagent apps.

(defn create-account [user-details error-ref]
  (let [validation-errors (do-validations user-details)]
    (if validation-errors
      (reset! error-ref validation-errors)
      (finish-create))))


(defn component []
   (let [user-details (atom {:email "" :password ""})
          ui-errors (atom nil)]
        (fn []
          [:div.Page
            (if @ui-errors
              [:p @ui-errors])
            [:input {:type "text" :placeholder "EMAIL" :on-change #(swap! user-details conj {:email (-> % .-target .-value)})}]
            [:input {:type "password" :placeholder "PASSWORD" :on-change #(swap! user-details conj {:password (-> % .-target .-value)})}]
            [:button.bottomMargin {:on-click #(create-account @user-details ui-errors)} "Submit"]])))

Where I'll set up an atom that handles storing any errors that happen and pass this into my signup flow. This is pretty simple, as a I can update the atom with any errors and show them to the user.

My concern is that this doesn't feel quite functional. I know I have this atom being updated in my block, and that feels like a side effect that ruins the purity of the function. I also dislike the need to pass the atom into my function for the sole purpose of being updated in an error state.

I suppose I will have to have side effect that at some point in my code if I want to show an error to my users, but I wanted to see if there were any better or accept patterns for doing so. I feel like my implantation works, but isn't quite as functional as I like and could potentially be hard to maintain if the flow for processing or validating the data grew.

like image 899
newBieDev Avatar asked May 31 '26 06:05

newBieDev


1 Answers

Put state in a single atom in it's own namespace like this. Warning: I haven't actually tested this code.

(ns my.app.db
  (:require [reagent.ratom :as r]))

(def default-db {})

(def my-db (r/atom default-db))

(defn init-db! []
  (reset! my-db default-db))


(defn new-account [user]
  (let [validation-errors (do-validation user)]
    (if validation-errors
      {:valid? false :errors validation-errors}
      {:valid? true :user user})))


(defn create-account! [user]
  (swap! my-db assoc :account (new-account user)))

Then in you UI code:



(ns my.app.ui
  (:require [my.app.db :as db]))

(defn user-form [{:keys [valid? user errors]}]
  (fn []
    [:div.Page
      (if-not valid?
        [:p errors])
     [:input {:type "text" :placeholder "EMAIL"
              :on-change #(swap! db/my-db assoc-in [:account :user :email] (-> % .-target .-value))
              :default-value (:email user)}
      [:input {:type "password" :placeholder "PASSWORD"
               :on-change #(swap! db/my-db assoc-in [:account :user :password] (-> % .-target .-value))
               :default-value (:password user)}]
      [:button.bottomMargin {:on-click #(db/create-account! user)} "Submit"]]]))


(defn parent-component []
  (let [db @db/my-db]
    (fn []
      [user-form (:account db)])))

You probably also need to add a check if form is dirty, add another property to :account map, so that you don't try to display errors if user is nil. But things like this can get messy quickly, so I would advise you to use something like fork for form management, it works nicely with reagent.

like image 140
Simon Polak Avatar answered Jun 02 '26 19:06

Simon Polak