D3 with Reagent
I’ve been interested in using D3.js with Reagent for awhile. Reagent is wrapper around React.js that uses a simple syntax that makes it straightforward to create relatively sophisticated web applications without needing many details of whats happening under the hood. Furthermore, Reagent has re-frame, an excellent framework for managing state whose documentation alone makes it a treasure trove of knowledge and entertainment (thanks Mike Thompson!). With a bit of clojurescript knowledge, the re-frame documents can get you making complicated, interactive web applications in no time. But at some point you will want to do something that is not 100% clojurescript, like add a D3-backed component, and things can get tricky quickly.
Once you grok how to do it using stateful components is not too difficult, however it can be very frustrating the first time around. Its only plumbing but its hard to get everything connected up so that they work as expected. I’ve written a very simple clojurescript application in Reagent that uses D3 that can be viewed here source here) that lets you control the size and location of three circles Its the sort of simple SVG that would have been very simple to make using hiccup-syntax but under the hood, D3 is being used to draw and update the circles. Therefore, although this is a simple app, it demonstrates the important logic that allows you to have reactive webapps with all the niceties that immutable data and global state afford you with nice D3 components inside them. I will point out a few things that may make your life easier if you are doing this for the first time.
How do you pass modified data?#
Just about everything I know about stateful components comes from the re-frame wiki, Nils Blum-Oeste, and jszakmeister. In each case there is an example of how to create a stateful component but each is a bit different. The same central idea is that you need to create a react class and define its lifecycle events. And then you need to pass data to that class. The trick is in correctly passing the data so that REACT recognizes it, does its diffing and updates your page. If you don’t really understand how the data is passed, you may spend an obscene about of time trying to get it right.
The basic idea, as per re-frame is as follows.
- Create a component that subscribes to your application state.
- Create the stateful component that has defined lifecycle events and is passed updated data from the first component.
# from the re-frame wiki
#component 1 subscribes to data and passes it to component 2
(defn gmap-outer []
(let [pos (subscribe [:current-position])] ;; obtain the data
(fn []
[gmap-inner @pos])))
#component 2 defines the lifecycle events that determine how to respond as the data changes
(defn gmap-inner []
(let [....]
(reagent/create-class
{:reagent-render ...
:component-did-mount ...
:component-did-update ...
:display-name ...})))
If you look at the other examples you will notice that everyone seems to pass the data around differently. Is there a function argument or not? Does the function argument need to be a map and not a vector? Do you need to store the React DOM node id as an atom? These are the rabbit hole questions. Instead of walking you though all the failures I will show the solution I ended up using that should work well for many similar use cases.
One Solution#
#component 1 subscribes to data and passes it to component 2
(defn app []
(let [data (subscribe [:circles])]
(fn []
[d3-inner @data])))
# my core d3 function
(defn d3-inner [data]
(reagent/create-class
{:reagent-render (fn [] [:div [:svg {:width 400 :height 800}]])
:component-did-mount (fn []
(let [d3data (clj->js data)]
(.. js/d3
(select "svg")
(selectAll "circle")
(data d3data)
enter
(append "svg:circle")
(attr "cx" (fn [d] (.-x d)))
(attr "cy" (fn [d] (.-y d)))
(attr "r" (fn [d] (.-r d)))
(attr "fill" (fn [d] (.-color d))))))
:component-did-update (fn [this]
(let [[_ data] (reagent/argv this)
d3data (clj->js data)]
(.. js/d3
(selectAll "circle")
(data d3data)
(attr "cx" (fn [d] (.-x d)))
(attr "cy" (fn [d] (.-y d)))
(attr "r" (fn [d] (.-r d))))))}))
The main points:
- The subscription data is passed directly into the function
- The core reagent rendering function defined a DIV and and SVG
- The Component-did-mount function will call D3. D3 converts the clojurescript data to JS and plots it. Note: i do not access the React Dom although that might be cleaner. I simply use D3 as I normally would only with clojurescript syntax. If there are more than one selectable elements per page, you would need a unique ID - either specified by you or derived from the React component itself. See the other examples I linked for details.
- Component-did-update is a function that will check for new data, and if a diff is found it will run.
Checking the diff using lifecycle events was the most confusing thing. In this case, in #4, we use (reagent/arv this)
to basically force react to example the incoming data compared with the old data. I got this directly from jszakmeister although you may notice he also stores the DOM node in an atom. I would be lying if I told you why it works. But it works. You may also notice that in Nils’ example, he uses a function that takes no arguments and he then accesses the data using (reagent/props this)
. I tried this style and couldn’t get it to work. Go with what works right?
Wrapup#
In this basic example, you can control your entire APP with the Reagent/Reframe model and get all the benefits thereby conferred to you. But if you want to add a D3 component that can update itself, its relatively straightforward:
- pass your data into a component as you normally do with re-frame
- setup your D3 component in the
:component-did-mount
lifecycle - update your D3 component with new data in the
:component-did-update
by using the[_ data] (reagent/argv this)
construct to access new/diffed data.
Hope that helps someone avoid a time sink!