Fork me on GitHub
#reagent
<
2017-11-07
>
maleghast22:11:54

Anyone about..? I have a weird issue… I have created a 3-form component in my Reagent code so that I can embed LeafletJS maps into other Reagent components. The problem is that the first time I use it it does not appear to have access to my app-state ratom. It feels as though there must be a weird order of precedence thing that I just don’t get here, but anyway… Can anyone help?

maleghast22:11:22

(if I (println …) the data that I want to see in the parent component, I see the data in the console. If I (println …) for the data that I want to see in the 3-form component on first render I don’t see it - I get a nil - and then thereafter I do see it, and my maps get their markers. )

maleghast22:11:59

(happy to paste into a gist, if there’s anyone that thinks that they might be able to help and so wants to see the code)

manutter5122:11:18

When you say “3-form” do you mean a form 3 component, that you create with reagent/create-class?

maleghast22:11:05

Yeah, exactly that

maleghast22:11:23

I can paste it in if you like, or Gist it…

manutter5122:11:28

If it’s not too many lines you could try the Slack “Code or Text Snippet” tool to post it

maleghast22:11:38

(defn re-leaflet
  [mapname latitude longitude zoom-level markers]
  (let [dn (r/atom nil)
        instmap (r/atom nil)
        mapdata {:foo "bar" :bam "baz"}
        mn mapname
        lt latitude
        lg longitude
        z zoom-level
        producer-markers markers]
    (println "markers next")
    (println producer-markers)
    (r/create-class
     {:component-will-mount #(println "component-will-mount")
      :component-did-mount (fn [ref]
                             (reset! dn (r/dom-node ref))
                             (let [map (js/L.map @dn)
                                   mappositioned (-> map (.setView (array lt lg) z))]
                               (.addTo (js/L.tileLayer "http://{s}. mappositioned)
                               (doall
                                (for [producer-marker (get producer-markers mn)]
                                  (-> (js/L.circle
                                       (array
                                        (:lat producer-marker)
                                        (:long producer-marker))
                                       (clj->js {:color (:color producer-marker)
                                                 :fillColor (:fillColor producer-marker)
                                                 :fillOpacity 0.5
                                                 :radius (:radius producer-marker)}))
                                      (.addTo mappositioned)
                                      (.bindPopup (:name producer-marker)))))
                               (def instmap mappositioned)))
      :component-will-unmount (fn []
                                (r/unmount-component-at-node @dn))
      :display-name (str "Leaflet Map - " mapname)
      :reagent-render (fn [mapname latitude longitude zoom-level markers]
                        [:div {:style {:height 620}}
                         instmap])})))

maleghast22:11:38

(last param “markers” is being passed in by the parent component, and is__ a map)

manutter5122:11:20

Well, I can see one problem: you have a def inside your component-did-mount

manutter5122:11:04

(def instmap mappositioned)

maleghast22:11:10

Er, ok… That works, in so far as the map gets drawn, the only thing that’s not working is the markers being “absent” on the first render

maleghast22:11:02

the def-ed value is inside the :reagent-render function.

manutter5122:11:24

It might be related — you’re defining instmap at the top, in the let statement, but you’re also using def to create a different symbol with the same name.

manutter5122:11:54

I’m thinking you meant (reset! instmap mappositioned) there maybe?

maleghast22:11:22

Oh yeah, I need to pull that out of the let - using a ratom to hold the component gave massive errors in the console, that I wanted to eliminate. def works seamlessly.

manutter5122:11:35

What kind of errors were you getting?

maleghast22:11:41

And so I did that and now the errors are back, hang on…

maleghast22:11:56

react-dom.inc.js:17859 Uncaught Error: Objects are not valid as a React child (found: object with keys {options, _container, _leaflet_id, _containerId, _fadeAnimated, _panes, _paneRenderers, _mapPane, _controlCorners, _controlContainer, _onResize, _targets, _events, _handlers, _layers, _zoomBoundLayers, _sizeChanged, _initHooksCalled, zoomControl, attributionControl, boxZoom, doubleClickZoom, dragging, keyboard, scrollWheelZoom, touchZoom, _zoomAnimated, _proxy, _loaded, _zoom, _lastCenter, _size, _pixelOrigin, _firingCount, _layersMaxZoom, _layersMinZoom}). If you meant to render a collection of children, use an array instead or wrap the object using createFragment(object) from the React add-ons. Check the render method of `Leaflet Map - ghana`.
    at invariant (react-dom.inc.js:17859)
    at traverseAllChildrenImpl (react-dom.inc.js:16578)
    at traverseAllChildren (react-dom.inc.js:16606)
    at Object.instantiateChildren (react-dom.inc.js:4254)
    at ReactDOMComponent._reconcilerInstantiateChildren (react-dom.inc.js:10263)
    at ReactDOMComponent.mountChildren (react-dom.inc.js:10302)
    at ReactDOMComponent._createInitialChildren (react-dom.inc.js:6114)
    at ReactDOMComponent.mountComponent (react-dom.inc.js:5933)
    at Object.mountComponent (react-dom.inc.js:11409)
    at ReactCompositeComponentWrapper.performInitialMount (react-dom.inc.js:4774)

manutter5122:11:16

Ah, ok, good that’s fixable

maleghast22:11:32

What did I mess up?

manutter5122:11:49

just tryin to remember how to fix it, lol

manutter5122:11:04

Basically what the error is telling you is that in your :reagent-render function you’re creating a div with a child of instmap, which is an r/atom, and React doesn’t know how to render CLJS atoms

manutter5122:11:25

You want to tell Reagent to convert that to something React can understand

manutter5122:11:23

I think it might be as simple as changing instmap to [@instmap]

maleghast22:11:38

ok, I will try that, one sec…

manutter5122:11:09

The @instmap pulls the value out of the atom, and then putting it in square brackets tells Reagent to treat it like a DOM node

maleghast22:11:47

Yeah that REALLY doesn’t work…

maleghast22:11:29

component.cljs:126 Error rendering component (in edge.dashboard_app.chains > edge.dashboard_app.re_leaflet > Leaflet Map - global)
reagent$impl$component$do_render @ component.cljs:126
(anonymous) @ component.cljs:143
reagent$ratom$in_context @ ratom.cljs:37
reagent$ratom$deref_capture @ ratom.cljs:43
reagent$ratom$run_in_reaction @ ratom.cljs:504
Leaflet Map - global_render @ component.cljs:143
(anonymous) @ react-dom.inc.js:5199
measureLifeCyclePerf @ react-dom.inc.js:4479

manutter5122:11:57

Ok, hang on a second, gotta look something up

juhoteperi22:11:54

you shouldn't render the Leaflet object at all

maleghast22:11:17

Sure… I put [@instmap] back to @instmap inside the :reagent-render function and the components start working again (apart from no markers on first render) and i get this again:

react-dom.inc.js:17859 Uncaught Error: Objects are not valid as a React child (found: object with keys {options, _container, _leaflet_id, _containerId, _fadeAnimated, _panes, _paneRenderers, _mapPane, _controlCorners, _controlContainer, _onResize, _targets, _events, _handlers, _layers, _zoomBoundLayers, _sizeChanged, _initHooksCalled, zoomControl, attributionControl, boxZoom, doubleClickZoom, dragging, keyboard, scrollWheelZoom, touchZoom, _zoomAnimated, _proxy, _loaded, _zoom, _lastCenter, _size, _pixelOrigin, _firingCount, _layersMaxZoom, _layersMinZoom, _renderer}). If you meant to render a collection of children, use an array instead or wrap the object using createFragment(object) from the React add-ons. Check the render method of `Leaflet Map - global`.
    at invariant (react-dom.inc.js:17859)
    at traverseAllChildrenImpl (react-dom.inc.js:16578)
    at traverseAllChildren (react-dom.inc.js:16606)
    at flattenChildren (react-dom.inc.js:15216)
    at ReactDOMComponent._reconcilerUpdateChildren (react-dom.inc.js:10280)
    at ReactDOMComponent._updateChildren (react-dom.inc.js:10388)
    at ReactDOMComponent.updateChildren (react-dom.inc.js:10375)
    at ReactDOMComponent._updateDOMChildren (react-dom.inc.js:6357)
    at ReactDOMComponent.updateComponent (react-dom.inc.js:6171)
    at ReactDOMComponent.receiveComponent (react-dom.inc.js:6133)

juhoteperi22:11:15

Doesn't make any sense - you have already rendered the :div that you give to Leaflet and which leaflet will modify

manutter5122:11:57

I have to run, here’s a blog post about Reagent lifecycles and embedding non-CLJS libraries that I was looking at. Might be useful or at least entertaining. http://timothypratley.blogspot.com/2017/01/reagent-deep-dive-part-2-lifecycle-of.html

juhoteperi22:11:17

Also, you might be interested in using react-leaflet, in most cases that makes using Leaflet easy: https://github.com/metosin/komponentit/blob/master/example-src/cljs/example/map.cljs

maleghast22:11:18

@manutter51 - Thanks, will have a read…

maleghast22:11:11

@juhoteperi - I’ll take a look at that… So what should be in my :reagent-render function in that code above if not the LeafletJS object?

maleghast22:11:01

@juhoteperi - Ok, that eliminates all the errors; thanks 🙂 Now, how is it that the first time I use the component it does not seem to be able to see my @app-state?

maleghast22:11:55

I “invoke” it inside another component, one that can__ see my @app-state and that tries to pass in the map of markers. More to the point at every other use of the component the markers appear

juhoteperi22:11:39

are the markers empty first, and then updated e.g. after some network request is done?

juhoteperi22:11:52

you component currently doesn't update Leaflet if markers update

maleghast22:11:05

Before I first use the component I get the app-data in my parent init function. The component that calls “re-leaflet” can see that app-state and I pass the map of markers into the call to re-leaflet

maleghast22:11:10

Here’s the console output:

Dashboard - Chains
core.cljs:182 Loading Dashboard Client data
core.cljs:182 markers next
core.cljs:182 nil
core.cljs:182 component-will-mount
core.cljs:182 Congratulations - your environment seems to be working
client.cljs:57 Reload websocket connected.
core.cljs:182 Loading Supply Chain data for ClientID - 1 and ChainID - 1
core.cljs:182 Loading Supply Chain Aspect data for ClientID - 1 and ChainID - 1 and Aspect Name - production
core.cljs:182 markers next
core.cljs:182 {ghana [{:name Producer One, :lat 7.188, :long -1.708, :radius 10000, :color green, :fillColor #0f3} {:name Producer Two, :lat 6.591, :long -1.437, :radius 10000, :color #ffaa00, :fillColor #f30}], brasil [{:name Producer Three, :lat -4.39, :long -55.46, :radius 80000, :color green, :fillColor #0f3} {:name Producer Four, :lat -20.8, :long -49.35, :radius 80000, :color red, :fillColor #f03}], global [{:name Producer One, :lat 7.188, :long -1.708, :radius 10000, :color green, :fillColor #0f3} {:name Producer Two, :lat 6.591, :long -1.437, :radius 10000, :color #ffaa00, :fillColor #f30} {:name Producer Three, :lat -4.39, :long -55.46, :radius 80000, :color green, :fillColor #0f3} {:name Producer Four, :lat -20.8, :long -49.35, :radius 80000, :color red, :fillColor #f03}]}
core.cljs:182 component-will-mount
core.cljs:182 markers next
core.cljs:182 {ghana [{:name Producer One, :lat 7.188, :long -1.708, :radius 10000, :color green, :fillColor #0f3} {:name Producer Two, :lat 6.591, :long -1.437, :radius 10000, :color #ffaa00, :fillColor #f30}], brasil [{:name Producer Three, :lat -4.39, :long -55.46, :radius 80000, :color green, :fillColor #0f3} {:name Producer Four, :lat -20.8, :long -49.35, :radius 80000, :color red, :fillColor #f03}], global [{:name Producer One, :lat 7.188, :long -1.708, :radius 10000, :color green, :fillColor #0f3} {:name Producer Two, :lat 6.591, :long -1.437, :radius 10000, :color #ffaa00, :fillColor #f30} {:name Producer Three, :lat -4.39, :long -55.46, :radius 80000, :color green, :fillColor #0f3} {:name Producer Four, :lat -20.8, :long -49.35, :radius 80000, :color red, :fillColor #f03}]}
core.cljs:182 component-will-mount
core.cljs:182 Loading Supply Chain data for ClientID - 1 and ChainID - 1
core.cljs:182 Loading Dashboard Client data
core.cljs:182 markers next
core.cljs:182 {ghana [{:name Producer One, :lat 7.188, :long -1.708, :radius 10000, :color green, :fillColor #0f3} {:name Producer Two, :lat 6.591, :long -1.437, :radius 10000, :color #ffaa00, :fillColor #f30}], brasil [{:name Producer Three, :lat -4.39, :long -55.46, :radius 80000, :color green, :fillColor #0f3} {:name Producer Four, :lat -20.8, :long -49.35, :radius 80000, :color red, :fillColor #f03}], global [{:name Producer One, :lat 7.188, :long -1.708, :radius 10000, :color green, :fillColor #0f3} {:name Producer Two, :lat 6.591, :long -1.437, :radius 10000, :color #ffaa00, :fillColor #f30} {:name Producer Three, :lat -4.39, :long -55.46, :radius 80000, :color green, :fillColor #0f3} {:name Producer Four, :lat -20.8, :long -49.35, :radius 80000, :color red, :fillColor #f03}]}
core.cljs:182 component-will-mount

juhoteperi22:11:24

core.cljs:182 markers next core.cljs:182 nil

juhoteperi22:11:30

so the first render markers are empty

maleghast22:11:13

Yeah, but the data is already in a ratom after core.cljs:182 Loading Dashboard Client data

juhoteperi22:11:53

No idea where that is printed, maybe when starting to load the data instead of when it is done?

maleghast22:11:10

I would need to gist the whole file I think….

juhoteperi22:11:39

Anyone, you will probably need to update :component-will-update to handle markers updates

maleghast22:11:56

basically there is an init function that gets the data and then attempts to render a component. That component invokes this form-3 component as a child

juhoteperi22:11:06

After that it doesn't matter if the markers are empty at first

maleghast22:11:36

Oh, ok… So how do I do that..?

maleghast22:11:50

The tutorial I read (on the reframe wiki) about form-3 components said to not mess with the update lifecycle functions so I was avoiding any of that…

juhoteperi22:11:49

Re-frame tutorial doesn't talk about wrapping non-controlled elements into React lifecycle

maleghast22:11:15

not the core tutorial was a wiki page on this subject, hold on…

juhoteperi22:11:06

I don't have examples on this now but you need :component-did-update (fn [this [_ prev-props]] ...) and check if markers in (r/props this) and :prev-props changed

juhoteperi22:11:46

No that doesn't say anything about wrapping non-React component, it is only about creating native React components

maleghast22:11:25

I got this far using that information and a couple of pointers from people on the #clojure-uk channel

juhoteperi22:11:35

But as I said, you can use React-leaflet, and you don't need to care about lifecycle methods

maleghast22:11:02

I will go and look at that link in a sec - I didn’t want to throw away this work if I don’t have to…

juhoteperi22:11:09

simplest way to implement did-update would be to just remove all markers from Leaflet, and then add new ones, always

maleghast22:11:22

Oh, I tried react-leaflet - couldn’t get it to work

maleghast22:11:43

(I looked at your link and worked back to the cljsjs package)

juhoteperi22:11:04

As React presumes props are provided as single object, but you use 5 arguments, you need to use both props and children to get new and old argument values, maybe something like this:

(fn [this [_ prev-mapname [prev-latitude prev-longitude prev-zoom-level prev-markers]]
  (let [mapname (r/props this)
         [latitude longitude zoom-level markers] (r/children r]
  (when (not= prev-markers markers)
    (.removeLayer leaflet ...)
    (for [marker markers] ...))))

maleghast23:11:02

OK… I have no idea what you are suggesting here, which is probably because I have a very n00b (i.e. surface level only) understanding of React. I realise that this is not ideal, and under other circumstances I would want to immerse myself in React in order to really__ understand what Reagent is doing and so forth, but I am under a lot of time pressure… This is not me asking you to fix my code for me, I hasten to add. Are you saying that I can write a function that will effectively force the component to “look again” when the parent component passed in the map of markers and update itself?

maleghast23:11:42

It seems to have no problem(s) getting the other values from the parent component (the name, which is a string, and the lat, long and zoom which are all numbers), so why can it not “see” the map that I am passing in? The parent component can see it - I can (println …) the map in the parent component and the data is all there.

maleghast23:11:48

I think I see what’s happening…

Dashboard - Chains
core.cljs:182 Loading Dashboard Client data
core.cljs:182 markers being pushed into the re-leaflet component
core.cljs:182 nil
core.cljs:182 markers next
core.cljs:182 nil
core.cljs:182 component-will-mount
core.cljs:182 Congratulations - your environment seems to be working
client.cljs:57 Reload websocket connected.
core.cljs:182 markers being pushed into the re-leaflet component
core.cljs:182 {ghana [{:name Producer One, :lat 7.188, :long -1.708, :radius 10000, :color green, :fillColor #0f3} {:name Producer Two, :lat 6.591, :long -1.437, :radius 10000, :color #ffaa00, :fillColor #f30}], brasil [{:name Producer Three, :lat -4.39, :long -55.46, :radius 80000, :color green, :fillColor #0f3} {:name Producer Four, :lat -20.8, :long -49.35, :radius 80000, :color red, :fillColor #f03}], global [{:name Producer One, :lat 7.188, :long -1.708, :radius 10000, :color green, :fillColor #0f3} {:name Producer Two, :lat 6.591, :long -1.437, :radius 10000, :color #ffaa00, :fillColor #f30} {:name Producer Three, :lat -4.39, :long -55.46, :radius 80000, :color green, :fillColor #0f3} {:name Producer Four, :lat -20.8, :long -49.35, :radius 80000, :color red, :fillColor #f03}]}

maleghast23:11:30

Right… So I need to update the component that I’ve written, because the first time that the parent component is eval-ed it doesn’t see the app-state either. OK, I will go and figure out how to do this update inside the form-3 component. Thanks for putting me on the path, @juhoteperi

mikethompson23:11:53

@maleghast (I haven't followed the entire discussion but ..) is this the re-frame docs you were looking for? https://github.com/Day8/re-frame/blob/master/docs/Using-Stateful-JS-Components.md @juhoteperi

juhoteperi23:11:09

that one should be helpful, and the last note is good: > the props passed (in this case @pos) in must be a map, otherwise (reagent/props comp) will return nil. Using map as props is going to be simpler than using several arguments

maleghast23:11:26

I’ve changed over to using a map for the props, so hopefully I will be able to do this 🙂