Skip to content

Using Stateful JS Components

Mike Thompson edited this page Dec 8, 2015 · 39 revisions

You know what's good for you. You know what's right. But it doesn't matter - the wickedness of the temptation is too much.

The JS world is full of shiny component baubles: D3, Google Maps, Chosen, etc.

Of course, they are salaciously stateful and mutative. And, you, raised in a strictly functional home, with a stern immutable father, know they are wrong. But, my, how you still yearn for the sweet thrill of that forbidden fruit.

I won't tell if you don't. But careful plans must be made ...

The overall plan

If you want to use a stateful js component, you'll need to write two Reagent components:

  • The outer component is responsible for sourcing data via a subscription or r/atom or cursor, etc.
  • The inner function is responsible for actually wrapping and manipulating the stateful JS component via lifecycle functions.

Crucially the outer function (which sources data) then supplies that data through to the inner, via props.

Example Using Google Maps

(defn gmap-inner []
  (let [gmap    (atom nil)
        options (clj->js {"zoom" 9})
        update  (fn [comp]
                  (let [{:keys [latitude longitude]} (reagent/props comp)
                        latlng (js/google.maps.LatLng. latitude longitude)]
                    (.setPosition (:marker @gmap) latlng)
                    (.panTo (:map @gmap) latlng)))]

    (reagent/create-class
      {:reagent-render (fn []
                         [:div
                          [:h4 "Map"]
                          [:div#map-canvas {:style {:height "400px"}}]])

       :component-did-mount (fn [comp]
                              (let [canvas  (.getElementById js/document "map-canvas")
                                    gm      (js/google.maps.Map. canvas options)
                                    marker  (js/google.maps.Marker. (clj->js {:map gm :title "Drone"}))]
                                (reset! gmap {:map gm :marker marker}))
                              (update comp))

       :component-did-update update
       :display-name "gmap-inner"})))

(defn gmap-outer []
  (let [pos (subscribe [:current-position])]   ;; obtain the data
    (fn []
      [gmap-inner @pos])))

Notes:

  • gmap-outer obtains data via a subscription. It is quite simple. Trivial almost.
  • it then passes this data as a prop to gmap-inner. This inner layer has the job of wrapping/managing the stateful js component (Gmap in our case above)
  • when the data (delivered by the subscription) to the outer layer changes, the inner layer, gmap-inner, will be given a new prop - @pos in the case above.
  • when the inner layer is given new props, the entire set of lifecycle functions defined for it will be engaged.
  • the renderer for the inner layer ALWAYS renders the same, minimal container hiccup for the component. Even though the props have changed, the same hiccup is output. So it will appear to React as if nothing changes from one render to the next. The HTML is the same each time. No work to be done. It will leave the DOM untouched.
  • but this inner layer has other lifecycle functions. This is where the real work is done.
  • for example, after the renderer is called (which ignores its props), component-did-update will be called. In this function, we have a chance to update the stateful JS component using the contents of the new props.

Credit

This example was developed by @jhchabran in this gist: https://gist.github.com/jhchabran/e09883c3bc1b703a224d#file-2_google_map-cljs