Search code examples
clojurere-frame

How should one handle AJAX success/error responses in Clojure re-frame?


I love re-frame, but I realize that I'm having a bit of trouble finding a nice pattern for handling AJAX responses.

My situation is the following:

I have a "global" event handler that triggers some AJAX call and dispatches to some other global event handler, depending on whether that call was successful, e.g.:

(reg-event-db :store-value
  (fn [db [_ value]]
    (do-ajax-store value
                   :on-success #(dispatch [:store-value-success %])
                   :on-error   #(dispatch [:store-value-error %])
    db))

(reg-event-db :store-value-success
  (fn [db [_ result]]
    (assoc db :foobar result)))

(reg-event-db :store-value-error
  (fn [db [_ error]]
    (assoc db :foobar nil
              :last-error error)))

(I am aware of reg-event-fx and stuff, I'm just avoiding it here for the sake of brevity and because I think it does not make any difference for my problem).

I also have (multiple, distinct) UI components that might trigger the :store-value event, like so:

(defn button []
  (let [processing? (reagent/atom false)]
    (fn button-render []
      [:button {:class (when @processing? "processing")
                :on-click (fn []
                            (reset! processing? true)
                            (dispatch [:store-value 123]))}])))

So in this case the component has local state (processing?) that is supposed to depend on whether the AJAX call is still in progress or not.

Now, what is the proper pattern here to have the button component react to the events :store-value-success and :store-value-error in order to reset the processing? flag back to false after the AJAX call has finished?

Currently, I'm working around that problem by passing down callbacks but that seems really ugly because it clutters the event handlers' code with stuff that does not really belong there.

The best solution that I've thought of would be to have the button component being able to hook into the :store-value-success and :store-value-error events and install its own handler for those events, like this:

(defn button []
  (let [processing? (reagent/atom false)]
    (reg-event-db :store-value-success
      (fn [db _]
        (reset! processing? false)))
    (reg-event-db :store-value-error
      (fn [db _]
        (reset! processing? false)))
    (fn button-render []
      [:button {:class (when @processing? "processing")
                :on-click (fn []
                            (reset! processing? true)
                            (dispatch [:store-value 123]))}])))

However, that does not work. As it seems, re-frame does not allow multiple event handlers per event. Instead, a subsequent invocation of reg-event-db on one single event id will replace the previous event handler.

So how do you guys handle situations like this?


Solution

  • I think reg-event-fx (src) might indeed help solve your problem.

    You could add a subscription that watches app-state e.g.

    (rf/reg-sub
      :app-state
      (fn [db]
        (get db :app-state)))
    

    and add this to your button, perhaps with a state function e.g.

    (defn btn-state [state]
      (if (= state :processing)
         "processing"))
    

    And then in the AJAX handler, you could add an fx to update the state-

    (reg-event-fx              ;; -fx registration, not -db registration
      :ajax-success
      (fn [{:keys [db]} [_ result]]             
         {:db       (assoc db :app-state :default)         
         :dispatch [:store-value-success result]}))
    
     (reg-event-fx              ;; -fx registration, not -db registration
        :ajax-error
          (fn [{:keys [db]} [_ result]]             
             {:db       (assoc db :app-state :default)         
             :dispatch [:store-value-error result]}))
    

    and update the AJAX handler

    (reg-event-db :store-value
      (fn [db [_ value]]
        (do-ajax-store value
                   :on-success #(dispatch [:ajax-success %])
                   :on-error   #(dispatch [:ajax-error %])
        db))
    

    This would be one way to handle it via -fx. I think you have already started to see the need for tracking app state, and I think bumping it up into the subscriptions would help with complexity, at which point your button render is greatly simplified.

     (defn button []
          [:button {:class (btn-state @app-state)
                    :on-click (dispatch [:store-value 123]))}])))