om.next mutate of other-component state not causing other-component to re-render

238 views Asked by At

I am updating state in one of my mutations, and a piece of it is not used by this component, but is by another one. When I do the mutate I see the that the app-state is updated in the repl, and if I cause the component to re-render for other reasons, it will show correctly, but I can not get the mutate to schedule a re-render of the second component. In the example below clicking on a button should decrement the value near the color name in the second list, but it does not.

There is some examples showing using :value [k k] in the mutate return, but those throw an error, must be out of date tutorials, as the current format is :value {:keys [...]}, so says the code and some tutorials . However I can't find any part of om.next actually USING :keys as a keyword that isn't a destructure operation (so not using :keys as an actual keyword, but it is a common word so I may have missed one somewhere)

In the repl I see this for the app-state:

=> (om/app-state reconciler)
#object [cljs.core.Atom {:val 
  {:tiles [[:tile/by-pos "a7"]
           [:tile/by-pos "a9"]
           [:tile/by-pos "a11"]],
   :inventory [[:inv/by-color "red"]
               [:inv/by-color "blue"]
               [:inv/by-color "green"]],
   :tile/by-pos {"a7" {:pos "a7", :color nil},
                 "a9" {:pos "a9", :color nil},
                 "a11" {:pos "a11", :color nil}},
   :inv/by-color {"red" {:color "red", :remaining 2},
                  "blue" {:color "blue", :remaining 1},
                  "green" {:color "green", :remaining 1}}}}]

What am I missing?

(ns omnexttest.core
  (:require [goog.dom :as gdom]
            [om.next :as om :refer-macros [defui]]
            [om.dom :as dom]))

(defmulti read om/dispatch)

(defmethod read :default
    [{:keys [state] :as env} key params]
      (let [st @state ]
            (if-let [[_ value] (find st key)]
                    {:value value}
                    {:value :not-found})))

(defmethod read :tiles
    [{:keys [state] :as env} key params]
     {:value (into [] (map #(get-in @state %) (get @state key))) })

(defmethod read :inventory
    [{:keys [state] :as env} key params]
     {:value (into [] (map #(get-in @state %) (get @state key))) })

(defmulti mutate om/dispatch)

(defmethod mutate 'draw/edit-edge
  [{:keys [state] :as env} _ {:keys [this pos color]}]
    {:value {:keys [[:inv/by-color color :remaining]]}
     :action (fn []  (do
               (swap! state assoc-in [:tile/by-pos pos :color] color )
               (swap! state update-in [:inv/by-color color :remaining] dec)))})

(defn hex-color
  [ this pos color ]
    (om/transact! this `[(draw/edit-edge ~{:this this :pos pos :color color})]))

(defui TileView
    static om/Ident
    (ident [this {:keys [pos]}] [:tile/by-pos pos])
    static om/IQuery
    (query [this] '[:pos :color])
    Object
    (render [this]
      (let [{:keys [pos color] :as props} (om/props this)]
          (dom/li nil
            (str pos " " color)
            (for [color ["red" "green" "blue"]]
              (dom/button #js { :onClick (fn [e] (hex-color this pos color)) }
                       color))))))

(def tile-view (om/factory TileView {:keyfn :pos}))

(defui InvView
    static om/Ident
    (ident [this {:keys [color]}] [:inv/by-color color])
    static om/IQuery
    (query [this] '[:color :remaining])
    Object
    (render [this]
      (let [{:keys [color remaining] :as props} (om/props this) ]
        (dom/li nil (str color " " remaining)))))

(def inv-view (om/factory InvView {:keyfn :color}))

(def app-state {
                      :tiles [{ :pos "a7"  :color nil }
                              { :pos "a9"  :color nil }
                              { :pos "a11" :color nil }
                              ]
                      :inventory [{ :color "red" :remaining 2}
                                  { :color "blue" :remaining 1}
                                  { :color "green" :remaining 1}]
                      })

(defui MapView
       static om/IQuery
       (query [this]
              [{:tiles (om/get-query TileView)}
               {:inventory (om/get-query InvView) }])
       Object
       (render [this]
               (let [tiles (-> this om/props :tiles)
                     inv (-> this om/props :inventory) ]
                (dom/div nil
                  (dom/ul nil
                   (mapv tile-view tiles))
                  (dom/ul nil
                   (mapv inv-view inv))))))

(def reconciler
  (om/reconciler
    {:state app-state
     :parser (om/parser {:read read :mutate mutate})}))

(om/add-root! reconciler
  MapView (gdom/getElement "map"))

(defn on-js-reload []
  ;; optionally touch your app-state to force rerendering depending on
  ;; your application
  ;; (swap! app-state update-in [:__figwheel_counter] inc)
)
1

There are 1 answers

6
Chris Murphy On BEST ANSWER

The this that is passed into om/transact! is important for re-rendering, so here if this was for a MapView component then all three components would be re-rendered. You can have the function in MapView (thus using MapView's this) but call it from TileView. In TileView's render you need something like this:

{:keys [click-cb-fn]} (om/get-computed this)

When you call om/transact! re-rendering is done down from the component you pass as first argument - this. Thus, to take this to its extreme, you'll never have re-rendering problems if all om/transacts!s are done from the root component, and all functions are passed down via computed props.

But you don't have to pass functions down. An alternative is to keep them at the same component where the firing button is, and instead pass down (again via computed props) the parent component's this. All that matters is what component the first argument to om/transact! is - call om/transact! from where ever you like.

Follow on reads are another thing to be considered when thinking about re-rendering, but not for the example you gave - they are best considered when the component you need to be re-rendered is in a different subbranch of the render tree, where using a common root's this would not be practical.

Another thing to note is that a mutate's value is 'just for documentation'. So whatever you put there will have no effect.