Search code examples
clojureclojurescriptcore.asyncomsecretary

Clojurescript + Om: wait for state change, then do something


I am trying to make a Clojurescript app that shows recipes.

Relevant code follows (also available as a gist):

(defn load-recipes [data]
  (go (if (not (:loaded? @data))
        (let [recipes-data (<! (fetch-recipes data))]
          (om/update! data :recipes recipes-data)
          (om/update! data :loaded? true))
        (println "Data already loaded"))))

(defn define-routes [data]
  (defroute home-path "/" []
    (om/update! data :view :home))
  (defroute "/random" []
    (go (loop [loaded? (:loaded? (om/value data))]
          (if-not loaded? (do (println "Waiting for data...")
                              (recur (:loaded? (om/value data))))
                  (do (om/update! data :tag
                                  (rand-nth
                                   (vec (apply set/union (map :tags (:recipes @data))))))
                      (om/update! data :view :random)))))))

(defn app-view [data owner]
  (reify
    om/IWillMount
    (will-mount [_]
      (do
        (load-recipes data)
        (define-routes data)))
    om/IDidMount
    (did-mount [_]
      #_(fetch-recipes data))
    om/IRender
    (render [_]
      (html data))))

What I want to accomplish:

  • First fetch the recipes using an async http call. I am using cljs-http.client that returns a channel
  • Define routes using the secretary library. In the /random route I want to select a random recipe. This can only happen when the data has been fetched and has been updated in the application atom.

What I get now is an infinite loop in the browser. What's happening?

Another option is wrapping all my routes who need data fetching first in a go block and put (<! (load-recipes)) on the first line.

PS: I ended up with

(defn ensure-recipes-loaded [data]
  (go (if (not (:loaded? (om/value data)))
        (do (om/update! data :view :loading)
            (let [recipes-data (<! (fetch-recipes data))]
              (om/update! data :recipes recipes-data)
              (om/update! data :loaded? true)))
        (println "Data already loaded"))))

(defn define-routes [data]
  (defroute home-path "/" []
    (om/update! data :view :home))
  (defroute "/random" []
    (go
      (<! (ensure-recipes-loaded data))
      (do (om/update! data :tag
                      (rand-nth
                       (vec (apply set/union (map :tags (:recipes @data))))))
          (om/update! data :view :random))))
  (defroute "/random/:tagname" [tagname]
    (go (<! (ensure-recipes-loaded data))
        (om/update! data :tag tagname)
        (om/update! data :view :random)))

  (defroute "/recipe/:link" [link]
    (go (<! (ensure-recipes-loaded data))
        (om/update! data :view :recipe)
        (om/update! data :permalink link)))

  (defroute "*" [*]
    (go (<! (ensure-recipes-loaded data))
        (om/update! data :view :default))))

Solution

  • Using a loop construct without any (parking) channel operations inside it to wait is pretty much entirely contrary to the spirit of core.async. Keep in mind that JavaScript is single-threaded, so if you don't park a thread of execution, there's no opportunity for anything else to run.

    Have a channel that you close when the loading operation is complete. Try to read from that channel when you need to block; if it returns nil, it's done, and if it blocks, then you have the desired operation of waiting. If you want to periodically do something while waiting, then read from both that channel and a timeout.