[ANN] dumdom: A component library without React, API compatible with Quiescent


#1

dumdom is yet another “React for ClojureScript” style component library, but without the React dependency. It’s currently using snabbdom for the vdom layer, but the plan is to eventually port the whole thing to ClojureScript. dumdom is API compliant with Quiescent, and can be used as a drop-in replacement. It also supports hiccup markup and server-rendering, as well as inline CSS transitions. Check it out! https://github.com/cjohansen/dumdom

Feedback much welcome!


#2

@cjohansen

This is great.

What I like about the clojure/script community is that a library like yours helps me understand how to program at a lower level and makes it possible to build my own stack of libraries that I know inside and out.

I’d been wanting to replace React since it’s the last one in my stack that had been a blackbox for me. When I saw this post a few days ago while researching about virtual doms, I jumped on it and have spent many hours reading DOM APIs, Snabbdom, Quiescent, and your library. While I’ve neither fully comprehended nor spent any time actually using any of them, I want to share my current thoughts before they sink into oblivion.

First off, I really like the idea of using snabbdom to implement a purely functional architecture of Quiescent. I already have my core.async based state management library, which I currently plug into Rum components through its reactive mixins, and getting rid of component states would make separation of concerns much clearer. I found snabbdom simple and elegant. It’s a full circus of mutation and looping but I could at least follow the underlying logic based off of DOM APIs.

Then I tried to cross a bridge back to the clojurescript world and failed. I fell off. I feel like I got lost in semantics. With its imperative algorithm, snabbdom is complex enough that I could barely wrap my head around. But its language closely follows that of DOM APIs so I could at least understand when and where DOMElements get created, inserted, updated, destroyed, and removed. On the other hand, dumdom seems to emphasize its compatibility with Quiescent, which, if I understand correctly, draws its terminology from React. I had hard time translating dom lifecycle events between them and figuring out the same when and where of will-appear, did-appear will-enter, did-enter, etc. It’s not that explanation is lacking or code is hard to read, but that it was cognitively too much after coming all the way up from DOM APIs. Maybe this is a matter of getting used to but I personally felt the terms based on DOM APIs are easier to follow.

Second, how would you explain the comp-fn that defcomponent returns, if I ask you to rephrase it in a more descriptive manner? If I understand it correctly, dumdom puts an extra step on top of what snabbdom does. In the latter, you write hyperscript to create vnodes that contain all necessary data for real nodes. In the former, you write hiccup in place of hyperscript to return this comp-fn, which takes path and key to create vnodes. I’m wondering how important it is to add this extra step in your implementation of Quiescent APIs using snabbdom vdoms.

Finally, in dumdom.core/init-node!, why do you avoid replacing the target dom with a new one? I have at least noticed that holding on to the original element is useful in testing.

I hope I don’t come across overcritical. I just want to better understand what is going on at this low level of programming. I’ve learned a ton from reading your code and I thank you for that.


#3

Thanks for your feedback! Glad you found this all interesting enough to read through the code. The code to dumdom is not the easiest to grasp, as it is basically a big ball of state - so you have to mess around with it.

I’ll answer the easy question first:

Replacing the root node like Snabbdom does makes it hard to hold on to a reference to your app. If you create an element and render into it with snabbdom, the referenced element is no longer in the DOM, and you can’t use it to address your app. For this reason dumdom maintains the semantics of Quiescent/React and most other component libraries: the element you render the app into has all its contents wiped/replaced, but the node itself stays in the DOM. This way you can render into the same node everytime, you can apply class names to the root node etc.

You are right that Dumdom adds another layer on top of Snabbdom. The point of this layer is twofold:

  1. Remember the virtual DOM created in the previous render, and use it to avoid rendering even the virtual DOM when the underlying data hasn’t changed.
  2. Allow for the creation of custom components with “memory”, using the mechanism in 1)

In other words: virtual DOM libraries optimizes rendering by creating a virtual representation of the DOM and not making costly changes to the DOM when the structure is known to not change. Component libraries like dumdom further optimizes by not even creating a new virtual DOM representation when the data that creates the virtual DOM hasn’t changed.

The comp-fn in dumdom creates a closure over the previous vdom for the component, and knows the component’s relative position in the DOM tree using the path. Knowledge of this position allows dumdom to reuse virtual DOMs for elements that move around - typically if you reorder a list of items, the path can be used to make snabbdom just reorder DOM elements, instead of mutating or even recreating every DOM node in the list.

I hope this makes things a little clearer.

Not at all! I’m happy to answer questions, and glad you found it interesting enough to warrant such detailed feedback :slight_smile:


#4

Thanks for filling me in on what I didn’t understand.

I should’ve mentioned that all of my previous comments were about implementation details, which library users would not need to know about. Since APIs are simple, clean, and up to date, this library is, in my humble opinion, the best vdom library available in cljs for someone who does not rely on the React ecosystem and who knows how to wire up the “dumb” DOM to their state management system.

I think your goal of implementing a virtual dom in the style of functional programming without relying on React is a brilliant idea. What I find to be a major attraction of dumdum over the React rappers like reagent and rum is the fact that library users can read its source code and fully understand what is going on. The source code of snabbdom is not easy to read with imperative logic and low-level optimizations but it is definitely readable. And this attraction would become pronounced if you actually decide to replace the snabbdom implementation with your own.

In light of this argument that being able to read its implementation is a major attraction, I cannot help talking about its private details. To be frank, I found snabbdom easier to understand than dumdom, primarily because of the part of keeping track of DOM locations in closure. I feel that it would be easier to follow if the information about a DOM tree is simply put in a big global atom. I’m saying this out of my gut feeling and without a deeper thought on library design, so please ignore it if I’m missing a point.

Anyways, I look forward to further developments on this library.

I also want to mention your recent blog post on how to run figwheel-main with tools.deps and cider. I was tripping over on CIDER getting hanged because the :main-opts option was in the :dev profile and your blog saved my day. THANK YOU!


#5

I have to chime in here, because I feel this library needs more attention. Server-side rendering positions dumdom as a viable alternative to Rum for many projects I can imagine working on. This is a good thing, because that space needs more exploration; especially from players that do not want to rely on React. In fact, I seem to remember something about @tonsky only using React because that was the best virtual DOM implementation at the time - please correct me if I am wrong. :slight_smile:

Nice work, I look forward to seeing in which direction you take dumdom. :+1:


#6

I see. Well, I’m certainly open for concrete suggestions for how to improve the internals of dumdom. I’m not sure if keeping the whole structure in a single atom would be a feasible approach though, as inside any given component you don’t really know where you are - this is why path is passed into each component. But if you have better ideas please open a PR!

Glad it could be of assistance, I tripped a lot over this myself :face_with_head_bandage:


#7

I just realized it’s not fair to compare dumdom with snabbdom. As you articulated it earlier, one is a component library and the other a virtual DOM library. I’ve mixed up the two.

I’m trying to implement a simple virtual DOM library by building upon this minimal exercise by heiskr/prezzy-vdom-example and putting flesh on it. I’ll talk to you if I learn something remotely useful.


#8

A similar project: https://bitbucket.org/sonwh98/mr-clean


#9

AFAICT this does no vdom diffing, which means every change is directly flushed to the DOM. Haven’t tried it or benched it, but that seems like it would cause excessive DOM updates?


#10

Thank you for sharing the tutorial! I didn’t know you could configure CIDER using .dir-locals.el… Sweeet! :slight_smile: