hyperfiddle/electric

A question on the electric reactive behavior

philoskim opened this issue · 4 comments

I want to disable the below button for 3 seconds of server delay like the following.
However the client button isn't disabled at all on click.
How can I disable the button for 3 seconds of server delay?

(ns user.tutorial-7guis-1-counter
  (:require
    [hyperfiddle.electric :as e]
    [hyperfiddle.electric-dom2 :as dom]))

#?(:clj (def counter! (atom 0)))
(e/def counter (e/server (e/watch counter!)))

#?(:cljs (def disabled! (atom false)))
(e/def disabled (e/client (e/watch disabled!)))

(e/defn Counter []
  (e/client
    (dom/button (dom/props {:disabled disabled})
                (dom/on "click" (e/fn [e]
                                  (e/client (reset! disabled! true))
                                  (e/server
                                    (Thread/sleep 3000) ;; intentional delay
                                    (swap! counter! inc))
                                  (e/client (reset! disabled! false))))
      (dom/text "Count"))
    (dom/text (e/server counter))))

Calling Thread/sleep directly blocks the reactive (async) execution, try this instead: (new (e/task->cp (m/sleep n))) (uses missionary async sleep). I think that will work.

You can also get rid of the atom by putting (dom/props {:disabled true}) inside the e/fn which will mount the prop when the callback starts, and unmount the prop when the callback completes.

Here's a working example of that pattern with some explanation, see L33 (dom/style {:background-color "yellow"}) ; loading in https://electric.hyperfiddle.net/user.demo-chat-extended!ChatExtended

P.S. We aren't fully satisfied with ui/button semantics yet so this "callback" idiom may change in coming months.

Thaks a lot for quick answer and your revolutionary work for Electric!

I modified the source code as the following.

(e/defn Counter []
  (e/client
    (dom/button (dom/props {:disabled false
                            :class "aaa bbb"
                            :style {:background-color "green"}})
                (dom/on "click" (e/fn [e]
                                  (dom/props {:disabled true
                                              :class "ccc ddd"
                                              :style {:background-color "yellow"} })
                                  (e/server
                                    ;(new (e/task->cp (m/sleep 3000)))
                                    (Thread/sleep 3000)
                                    (swap! counter! inc))))
      (dom/text "Count"))
    (dom/text (e/server counter) )))

The server delay worked and the button is disabled during the delay as expected.
However I discovered another problem.

After running the above source code,

(1) At first, the button's class attribute has "aaa bbb" and the background-color is "green".

(2) After click, the button's class attribute is changed to "ccc ddd" and the background-color is "yellow".

(3) After the server delay finishes, the button's class attribute "ccc ddd" disappears and doesn't restore to "aaa bbb". The background-color disappears as well and changes to the default "light gray" button color, not "green" color.

I expected it should be restored to (1) state after click event finished.

Is this a bug in Electric or my misunderstanding?

Current dom/props behavior is to clear the props, so when the e/fn unmounts (finishes) the inner dom/props call clears them.

Your intuition in this example is correct, but the problem isn't that simple, e.g. what if the outer props call has a reactive value as the class and it changes while the callback is running?

We're still exploring what would "correct" behavior mean in the general case.

Here is how I'd write this today

(e/defn Counter []
  (e/client
    (dom/button
      (let [busy (try (dom/on "click" (e/fn [e]
                                        (e/server
                                          (case (new (e/task->cp (m/sleep 3000)))
                                            (swap! counter! inc)))))
                      false
                      (catch hyperfiddle.electric.Pending _ true))]
        (dom/props {:disabled busy
                    :class (if busy "ccc ddd" "aaa bbb")
                    :style {:background-color (if busy "yellow" "green")}}))
      (dom/text "Count"))
    (dom/text (e/server counter))))

While the callback is running it's throwing Pending, we can catch that and turn the whole thing into a busy flag. Now we can write a single props call.

Note 1 more change - as Dustin pointed out calling Thread/sleep is a bad idea. Electric runs in a single thread asynchronously, if you put the thread to sleep the whole runtime is sleeping.

If you want to keep the click handler alive for 3 seconds and then increment the counter you can wait for the sleep to resolve with case as above to sequence the calls. case will only run the else branch once the value isn't Pending (throwing to be more accurate).

Another factoring could increment when the DAG is unmounting

(e/server
  (new (e/task->cp (m/sleep 3000)))
  (e/on-unmount #(swap! counter! inc)))

Both of these work ;)