Reagent: What is the proper way of defining component properties?

1,785 views
Skip to first unread message

Ilya Boyandin

unread,
Sep 26, 2014, 5:43:24 AM9/26/14
to clojur...@googlegroups.com
Hello everyone!

I implemented a slider component in Reagent + D3 using reagent/create-class:

(ns slider
(:require [reagent.core :as reagent]
[reagent.impl.util :as util]))

(defn slider [{:keys [name width height value on-change]}]
(let [w (or width 150)
h (or height 20)]

(reagent/create-class {
:component-did-mount
(fn [this]
(let [pos->value #(-> js/d3 (.mouse (.getDOMNode this)) first (/ w) (min 1) (max 0))
on-drag #(on-change (pos->value))
drag (-> js/d3 .-behavior .drag
(.on "drag" on-drag))]
(-> js/d3
(.select (.getDOMNode this))
(.call drag))))

:render (fn [this]
(let [value (-> this util/get-props :value)]
[:svg {:width w :height h}
[:g {:className "slider"}
[:rect {:className "slider__rect" :x 1 :y 1 :width (- w 3) :height (- h 3) }]
[:rect {:className "slider__fill__rect"
:x 2 :y 2
:width (- (* w value) 4) :height (- h 4) }]
[:text {:className "slider__text" :x (/ w 2) :y (/ h 2)} name]]]))})))


Here the properties of the component are specified in the outer "slider" function. The values of these function params, however, are not getting updated when accessed from the inner render function on subsequent calls, of course. So the value of the parameter "value" always stays the same as when the component was first created, even if the actual property has changed. This is ugly and was a source of confusion for me.

I could resort to calling reagent.impl.util/get-props to get the most current property values in "render", and it works. However, get-props seems to be an internal function, so it is probably not intended to be called directly.

When using the alternative way of constructing components with "with-meta", the properties are passed as parameters directly to the render function. Hence, render always sees the most current values:

(def slider
(with-meta
(fn [{:keys [name width height value on-change]}] ...)

{:component-did-mount
(fn [this] ...)}))

This way there is at least less confusion, because there are no variables with obsolete property values here. But component-did-mount does not get the properties directly and has to use get-props again.

Personally, I would prefer using create-class for components with extra life-cycle functions, but this confusion with the variables getting obsolete makes it much less attractive than it could be.


Am I missing something obvious? What is the proper way of defining component properties which allows all the life-cycle functions to see them?

Thanks in advance!
Ilya

Nils

unread,
Mar 7, 2015, 4:10:07 PM3/7/15
to clojur...@googlegroups.com

Ilya, did you figure something out to do this in a nicer way?
I am struggling with this too.



Mike Thompson

unread,
Mar 7, 2015, 6:44:15 PM3/7/15
to clojur...@googlegroups.com, nblum...@googlemail.com
On Sunday, March 8, 2015 at 8:10:07 AM UTC+11, Nils wrote:
> Ilya, did you figure something out to do this in a nicer way?
> I am struggling with this too.


This should help:
https://github.com/Day8/re-frame/wiki/Creating-Reagent-Components#form-3-a-class-with-life-cycle-methods

Nils Blum-Oeste

unread,
Mar 8, 2015, 1:51:06 AM3/8/15
to clojur...@googlegroups.com, nblum...@googlemail.com
Thanks Mike, I had seen that already. IMHO it does not really explain this properly because props are only used in the render function in that example but not in the lifecycle hooks.

My current solution is to use `reagent.core/props` (public api, not an impl. like get-props, just a thin wrapper for it though) in the hooks. Here is a small working example. Pay attention to the usage of `props` in the first let block.

Is this supposed to be the idiomatic way to do it?

(defn- plot
([comp]
(let [data [{:label "foo"
:data (-> comp reagent/props :data)}]
(.plot js/$ (reagent/dom-node comp) (clj->js data) (clj->js plot-options)))))

(defn plot-component []
(reagent/create-class
{:component-did-mount plot
:component-did-update plot
:display-name "plot-component"
:reagent-render (fn []
[:div.plot-container {:style {:width "100%"
:height "500px"}}])}))

;; use the component like this:
; [plot/plot-component {:data (:plot-data @app-state)]]]])


As you can see, I do not have explicit props in the component function parameters even though I pass it when using the component. Is that bad practice?

I was struggling to get this whole thing working because I had another, only slightly related bug: I was passing props to the component not as a map (as can be seen above), but used a vector directly (the only prop I needed). This caused weird behaviours.

I put out a small blog post about this: http://nils-blum-oeste.net/clojurescripts-reagent-using-props-in-lifecycle-hooks/

Going to add an example to https://github.com/Day8/re-frame/wiki/Creating-Reagent-Components too if this gets confirmed to be the right way to do it.

Mike Thompson

unread,
Mar 8, 2015, 6:17:00 AM3/8/15
to clojur...@googlegroups.com, nblum...@googlemail.com
On Sunday, March 8, 2015 at 5:51:06 PM UTC+11, Nils Blum-Oeste wrote:
> Thanks Mike, I had seen that already. IMHO it does not really explain this properly because props are only used in the render function in that example but not in the lifecycle hooks.
>
> My current solution is to use `reagent.core/props` (public api, not an impl. like get-props, just a thin wrapper for it though) in the hooks. Here is a small working example. Pay attention to the usage of `props` in the first let block.
>
> Is this supposed to be the idiomatic way to do it?
>
> (defn- plot
> ([comp]
> (let [data [{:label "foo"
> :data (-> comp reagent/props :data)}]
> (.plot js/$ (reagent/dom-node comp) (clj->js data) (clj->js plot-options)))))
>
> (defn plot-component []
> (reagent/create-class
> {:component-did-mount plot
> :component-did-update plot
> :display-name "plot-component"
> :reagent-render (fn []
> [:div.plot-container {:style {:width "100%"
> :height "500px"}}])}))
>
> ;; use the component like this:
> ; [plot/plot-component {:data (:plot-data @app-state)]]]])
>
>
> As you can see, I do not have explicit props in the component function parameters even though I pass it when using the component. Is that bad practice?


Okay, I get it now. You want your lifecycle functions to get access to the latest props. The goal is give the js library (which really owns the div) the data it needs to redraw the component.

I've never had to do this, so I'm not sure what idiomatic would be, but:
- there's nothing wrong using that public API "props" etc
- it looks like your approach is used within the React world, so that's a good sign: http://nicolashery.com/integrating-d3js-visualizations-in-a-react-app/

Just in case, you could create a ticket on Github and see what feedback you get there: https://github.com/reagent-project/reagent/issues/


>
> I was struggling to get this whole thing working because I had another, only slightly related bug: I was passing props to the component not as a map (as can be seen above), but used a vector directly (the only prop I needed). This caused weird behaviours.

Remember that any vector is going to be interpreted as a component. Even when you don't want it to be. :-)

[X Y Z] is always going to be seen as a component X with props Y and Z, even if you just want that vector to be regarded as pure data for the surrounding component. Such is Hiccup.

--
Mike

Reply all
Reply to author
Forward
0 new messages