published Jan 03, 2017
So you’re enjoying React with its functional abstractions and uni-directional data flow. And yet you still crave direct DOM manipulation. You ignore the warnings, you know your karma will suffer, but you insist. Now what?
In truth, of course, there are legitimate reasons for reaching into
the DOM. Sometimes you need to sidestep React for performance reasons.
But a more common reason is that some DOM elements have an inherently
imperative API. For example, the HTML5 <video>
element has methods, .play
and .pause
, that
change playback status.
These are methods of the DOM element. However, when you’re building a component in React, you don’t usually touch the DOM itself. Instead, React components figure in an internal data structure that mirrors the hierarchy of the DOM but shields you from its details.
This hierarchy of elements consists entirely of immutable values, which is what gives React’s model its simplicity. Still there’s no denying that pausing a video will inevitably involve mutating state in some form.
Components are React’s core abstractions. A component class, of course, can be instantiated many times on any given page. The concrete representation of the page element on the JavaScript heap is called the component’s backing instance. For custom components written in JavaScript (or ClojureScript), this instance is a JavaScript object, which can have methods as well as props and state. But not everything can be a custom component, or nothing would ever reach your browser window. The backing instance of native components such as divs or videos is simply its DOM node.
If all we ever see in React-land is elements and components, how can we access the backing instance associated with a video elemento? The answer is to attach a callback ref to the element:
defn video-ui []
(let [!video (atom nil)] ;; clojure.core/atom
(fn [{:keys [src]}]
(:div
[:div
[:video {:src src
[:style {:width 400}
:ref (fn [el]
reset! !video el))}]]
(:div
[:button {:on-click (fn []
[when-let [video @!video] ;; not nil?
(if (.-paused video)
(
(.play video)
(.pause video))))}"Toogle"]]])))
When the rendering function is called initially, the video DOM node
has not been created yet. By passing the special ref
prop
to a component, you signal React your intent to be notified once the DOM
node has been created. Conveniently, the anonymous function receives as
its argument the backing instance. A useful pattern is to store the
reference to the node in an atom for later use.
Note that the ref callback is called twice in the component’s lifetime: first when the DOM element is created and, second, when it is destroyed. In the second case, React simply passes nil to the callback. For this reason it’s good practice to check the atom contents before accessing its attributes and methods, a precaution that prevents TypeErrors. After performing this check, the button’s on-click handler calls the appropriate method on the DOM node.
A few notes on the implementation:
We use a clojure.core/atom instead of a ratom to store the ref, as we don’t deref the atom in the render function or want the component to re-render when the video node is created.
The component is implemented as a Form-2
component. In this style, props need to be specified as arguments to
the inner (rendering) function. The outer (component-creating) function
often does not need to see the props. Using the fact that JavaScript
allows you to call functions with the wrong arity, we simply leave out
the src
prop in the declaration.
Earlier versions of React only supported string refs. Callback refs, introduced in recent versions of React, are elegant and a better fit for Reagent, so you should prefer them where you can. String refs and the related findDOMNode or r/dom-node API are now considered an anti-pattern.
The next step in building a video player could be to wrap this
stateful, mutating beast in a clean React component. Playback state
could be represented using a playing?
prop. This is a
powerful React pattern: encapsulate messy DOM-manipulation in a reusable
component with a clean, functional interface. This final exercise,
however, is left to the reader.
The code for this example is available on github.
React comes with good documentation on refs and backing instances.
For an in-depth explanation of React component from a ClojureScript perspective, the Lambda Island episode on React is excellent (paywalled but with a free trial available).
React From Zero is a useful step-by-step guide to React’s concepts in JavaScript
This is presumably for side-effects, a blog by Paulus Esterhazy. Don't forget to say hello on twitter or by email