Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Clojurescript, Reagent: pass atoms down as inputs, or use as global variables?

I'm writing a Clojurescript app, using Reagent to make my components reactive.

I have a simple question. Should I

  1. Pass my atoms as inputs through my components, or
  2. Use the atoms as global variables and let them 'side-affect' my components?

In the tutorial they use the latter option, however in trying to keep my functions pure I opted for the former.

Am I correct in saying that using them as global variables (in addition to being less verbose when defining component inputs) prevents re-rendering of whole parent components where the atom's state is not used?

like image 365
Boyentenbi Avatar asked Jul 11 '16 10:07

Boyentenbi


2 Answers

If you make your components accept atoms as arguments, then you can make them much more reusable and easier to test.

This is especially true if you opt for keeping your entire application state in a single atom, then passing it down to child components using cursors.

;; setup a single instance of global state
(defonce app-state
  (reagent/atom {:foo 0 :bar 0})

;; define a generic counter component that knows
;; nothing about the global state
(defn counter
  [count]
  [:div
    [:button {:onclick #(swap! count inc) "+"]
    [:span @count]])

 ;; define counter components and give them access to
 ;; specific context within the global state
 (defn app
   [state]
   [counter (reagent/cursor app-state [:foo])]
   [counter (reagent/cursor app-state [:bar])])

You can even go one step further if you decide to use Reagent with Re-frame. Re-frame encourages you to build your app with a specific architecture that looks something like this.

 app-db  >  subscriptions
   ^             
handlers        v
   ^             
 events  <  components
  1. Rather than just writing components and hooking them straight up to a global atom (app-db), you write subscriptions which are just functions that select/query some data from app-db and pass it along to components whenever app-db is changed.

  2. Then, rather than a component messing around with app-db directly, the component creates events which are just small pieces of data that describe the intent of the component.

  3. These events are sent to the handlers, which are functions that take the event and the current app-db as arguments and return a new app-db. Then the existing app-db is replaced, triggering the subscribers to pass data down to the components and so on.

It's definitely helpful if you find your Reagent project getting a bit tangled up and the Re-frame readme is a great read, whether you decide to use it or not.

like image 184
Dan Prince Avatar answered Oct 12 '22 05:10

Dan Prince


I prefer passing a ratom to the component. Re-frame is becoming popular https://github.com/Day8/re-frame

Passing a ratom in does not make your function any more pure, the atom can still be side-effected. It does make your components more flexible and reusable because they define their dependencies.

It does not affect the re-rendering directly whether you refer to a global db or a passed in db. The signal graph of when to render is built from occurrences of deref inside the vector, which doesn't care about where the ratom comes from. However you can be more efficient by creating reactions.

(defn my-component []
  (let [x (reaction (:x @db)]
    (fn []
      [:div @x]))

This component will only re-render when :x changes (not when anything changes in db. Creating reactions can become tedious, which is one of the appeals of re-frame.

(ns whip.view.reactions
  (:require [reagent.core :as reagent]
            [devcards.core :refer-macros [defcard-rg deftest]])
  (:require-macros [reagent.ratom :refer [reaction]]))
(def a (reagent/atom {:x 100 :y 200})) (def b (reaction (:x @a)))
(def c (reaction (+ @b 10)))
(defn view-c []
  (prn "Rendering view-c") [:div
  [:div @c]
  [:button {:on-click (fn [e] (swap! a update :x inc))} "inc x"]
  [:button {:on-click (fn [e] (swap! a update :y inc))} "inc y"]])
(defcard-rg reaction-example [view-c])

Reactions are a very concise way to express data ow. Here you start with a ratom containing x and y values. You then build a reaction b that only observes the x value. Next, introduce another reaction c that observes the sum of b and 10. Then create a component that renders c reactively. Observe that when you click the “inc x” button, the view is updated with the result of applying the expressions. When you click the “inc y” button, nothing happens. Check the console to con rm that the “Rendering view-c” message is only printed when clicking “inc x”. This is a very good thing, because the view does not depend on y in any way. If you were to deref a instead of c in the view, it would be re-rendered even if y changed. Reagent reacts to reactions and ratoms via requestAnimationFrame. So, changing many ratoms and reactions that depend on them only results in one render phase.

like image 28
Timothy Pratley Avatar answered Oct 12 '22 07:10

Timothy Pratley