Let Sub Component React on Parent's State Plus Having its Own State - clojurescript

Consider the following Reagent components:
(defn sub-compo [n]
(let [state (r/atom {:colors (cycle [:red :green])})]
(fn []
[:div {:style {:color (-> #state :colors first)}
:on-mouse-move #(swap! state update :colors rest)}
"a very colorful representation of our number " n "."])))
(defn compo []
(let [state (r/atom {:n 0})]
(fn []
[:div {:on-click #(swap! state update :n inc)}
"Number is " (#state :n) "."
[sub-compo (#state :n)]])))
I tried to make up an example, in which a sub component should depend on the state of its parent component. However the sub component should have an internal state as well. The above does not work properly. When the state in compo changes sub-compo is not initialized a new.
Which would be the way to go here, in order to let sub-compo be in sync with comp? Whenever the state of comp changes sub-comp should actually be initialized anew, meaning it's color state is set to the initial value again.
Here's a solution that does at least what I want. It uses a cursor and a watch. But maybe there is a simpler way to do so, anyways:
(defn sub-compo [n]
(let [init-state {:colors (cycle [:red :green])}
state (r/atom init-state)]
(add-watch n :my (fn []
(reset! state init-state)))
(fn []
[:div {:style {:color (-> #state :colors first)}
:on-mouse-move #(swap! state update :colors rest)}
"a very colorful representation of our number " #n "."])))
(defn compo []
(let [state (r/atom {:n 0})]
(fn []
[:div {:on-click #(swap! state update :n inc)}
"Number is " (#state :n) "."
[sub-compo (r/cursor state [:n])]])))

The above does not work properly. When the state in compo changes
sub-compo is not initialized a new.
This is because the inner function of sub-compo needs to receive the argument n as well.
Whenever the state of comp changes sub-comp should actually be
initialized anew, meaning it's color state is set to the initial value
again.
You could use :component-will-receive-props for this.
This worked for me:
(defn sub-compo [n]
(let [init {:colors (cycle [:red :green])}
state (r/atom init)]
(r/create-class
{:component-will-receive-props
(fn [this [_ n]]
(reset! state init))
:reagent-render
(fn [n]
[:div {:style {:color (-> #state :colors first)}
:on-mouse-move #(swap! state update :colors rest)}
"a very colorful representation of our number " n "."])})))

Related

Reagent Component not Re-Rendering on Prop Change

My Reagent component ist a simple div that has a component-did-mount and a component-did-update hook. It draws notes using vexflow.
(defn note-bar [notes]
(reagent/create-class
{:display-name "Note Bar"
:reagent-render (fn [notes]
^{:key notes} ;; force update
[:div#note-bar])
:component-did-mount (fn [this]
(draw-system-with-chord notes))
:component-did-update (fn [this]
(draw-system-with-chord notes))}))
It is used like this.
(defn exercise-one []
(let [note (re-frame/subscribe [:exercise-one/note])]
[:div
[note-bar/note-bar #note]
[other]
[components]]))
My event code is the following.
(defn store-exercise-one-note [db [_ note]]
(assoc-in db [:exercise-one :note-bar :note] note))
(re-frame/reg-event-db
:exercise-one/store-note
store-exercise-one-note)
(defn query-exercise-one-note [db]
(or (get-in db [:exercise-one :note-bar :note])
[{:octave 4 :key :c}]))
(re-frame/reg-sub
:exercise-one/note
query-exercise-one-note)
I verified that the app-db value changes using 10x. Yet the note bar only displays a different note when Hot Reloading kicks in. I believe this is due to the component-did-update hook not being called.
My question is, is this the right way to bind a JavaScript library that renders something? If so, why does my component not update?
The following fixed the component. See the documentation about form-3 components here
(defn note-bar [notes]
(reagent/create-class
{:display-name "Note Bar"
:reagent-render (fn [notes]
^{:key notes} ;; force update
[:div#note-bar])
:component-did-mount (fn []
(draw-system-with-chord notes))
:component-did-update (fn [this]
(let [new-notes (rest (reagent/argv this))]
(apply draw-system-with-chord new-notes)))}))

Render two components in reagent / reframe

I have some code:
(defn second-panel []
[:div
[:h1 "Hello "]
])
(defn root-container []
(second-panel)
(let [name (re-frame/subscribe [::subs/name])]
[:div
[:h1 "Hello from " #name]
]))
but only the root-container component renders. Why is my second-panel function not rendering?
You have to add (second-panel) to your div. The return value of (second-panel) is currently ignored.
(defn root-container []
(let [name (re-frame/subscribe [::subs/name])]
[:div
(second-panel)
[:h1 "Hello from " #name]
]))
The correct solution to returning multiple virtual DOM elements from a function without wrapping them in a container element is to use a Fragment. In reagent this is handled by the :<> special keyword.
(defn second-panel []
[:div
[:h1 "Hello "]])
(defn root-container []
(let [name (re-frame/subscribe [::subs/name])]
[:<>
[second-panel]
[:div
[:h1 "Hello from " #name]
]]))
;; or, with the nested let. both variants are fine.
(defn root-container []
[:<>
[second-panel]
(let [name (re-frame/subscribe [::subs/name])]
[:div
[:h1 "Hello from " #name]
])])
There is also a different in (second-panel) and [second-panel] since (second-panel) will actually call the function directly which means it will not behave like a regular reagent function but instead become part of the caller. You should prefer the [second-panel] notation for all "component" type functions.

form-3 component not rerendering anything even though :component-did-update is called

I have the following code to test out form-3 components:
(defn inner [data]
(reagent/create-class
{:display-name "Counter"
:component-did-mount (fn []
(js/console.log "Initialized")
[:h1 "Initialized! " data])
:component-did-update (fn [this _]
(let [[_ data] (reagent/argv this)]
(js/console.log (str "Updated " data))
[:div (str "My clicks " data)]))
:reagent-render (fn [] [:div (str "My clicks " data)])}))
I am able to successfully trigger both the :component-did-mount and the :component-did-update as there is the expected output in the console log. However, neither of the two functions actually change anything on the page. It just shows the initial state of [:div (str "My clicks " data)] the whole time.
What am I doing wrong? Ps. I have read the reagent docs and the purelyfunctional guide.
You have to repeat the parameters in the :reagent-render function:
(fn [data] [:div (str "My clicks " data)])

Reagent state not updating after setInterval

I have this reagent component that uses setInterval to change its state:
(defn foo []
(let [value (atom 1)]
(js/setInterval (fn [] (reset! value (rand-int 100)) (println #value)) 1000)
(fn []
[:p #value])))
I can see the value being printed, a different one each time, but the html doesn't change. Why is that?
And the answer is that I should have been using a reagent.core/atom instead of an atom.

How to override onload methods in ClojureScript?

I am trying to override onload function of document and Image in ClojureScript. I think that set! should be possible to do it, but i am not getting any success. Relevant code is as follows :
(defn load-image [img-path]
(let [img (js/Image.)]
(do (set! (.-src img) img-path)
img)))
(defn add-img-canvas [img-path width height]
(let [img (load-image img-path)]
(set! (.-onload img)
(fn [] ;; This function is never called.
(let [canvas (get-scaled-canvas img width height)]
(do (pr-str canvas)
(swap! game-state :canvas canvas)))))))
(defn hello-world []
(let [count (atom 1)]
(fn []
[:div
[:h1 (:text #game-state)]
[:div (do (swap! count inc) (str "count is " #count))]
[:canvas (:canvas #game-state)]])))
(reagent/render-component [hello-world]
(. js/document (getElementById "app")))
(set! (.-onload js/document)
(fn [] ;; This function is also never called.
(add-img-canvas (:img-src game-state) 100 130)))
;;(. js/document onload)
Anonymous functions in add-img-canvas is not getting called. What am i doing wrong ?
I think it may be down to the difference between document.onload vs window.onload. The latter does work as expected.
See this for more details between the two.