The question pretty much says it all, and is obviously only directed only to those of you to whom it applies.
Here’s what I’m interested in finding out:
How did you develop using React?
Was there something specific you were trying to do and found you couldn’t do, or couldn’t do easily, in React?
What have you used subsequently instead of React, and how did it help to address any of the problems you encountered when using React?
This post isn’t intended to be a dig at React or any of the wrappers, but is more about understanding any shortcomings that people have encountered and how they addressed them.
Here are my answers:
I’ve only ever used Reagent, which at the time I started using it a few years ago seemed to provide a much nicer interface to React than the now-deprecated Om.
there were often times when I would trigger state changes and these weren’t picked up by the UI. I really wanted to be able to express 100% of the time that a given state change would produce a given result, but it was either due to my poor understanding, or something in the framework(s), but I wasn’t able to fully guarantee the DOM would update when it should have
the virtual DOM worked reasonably well for a small DOM tree, but as the tree grew in size and complexity, it seemed at least to me to take a lot longer to render than it could have
at least at the time I used it (2-3 years ago), Reagent was interpreting the Hiccup tree, which I feel could have been optimized using macros to pre-compile (into e.g. React.createElement() calls and so on)
at the time I was using it, there wasn’t decent support for Server-Side Rendering
at the time I was using it, there wasn’t a decent solution for CSS
I built my own framework which addresses all of these issues and more, and provides additional functionality which is designed to simplify and expedite web development in general. I should add: This post is not intended to promote said framework, it’s more for research purposes.
If this question applies to you, I would love to hear your answers, especially any challenges you had working with React.
We are still using Reagent. Switching to something else would be too expensive. However, I would like to get rid of React in the long run, since they changing the framework fundamentals too often. We never started to use hooks and with the high adoption rate of signals I would not wonder if hooks are also considered “deprecated” soon. If you glimpse a bit then I would say that signals are the same what Reagent is already doing since almost a decade.
At the moment I find dumdom the most interesting alternative. It’s like hiccup + a vdom library. But the important point is that it does not mess up the hiccup data returned by your function. And this allows very interesting and data-driven solutions for event handlers and internationalization. This talk covers it all:
Before leaving I wasn’t using Reagent or any hiccup based variants, since they’ll always be way too slow for my performance obsessed self. I used a bad macro based wrapper I wrote myself, and a variety of other wrappers testing various ways of doing them.
I stopped using react many years ago, pretty much when hooks were introduced. Not because hooks were a bad idea, but because of the churn this created. At the time I felt hooks were a step in the wrong direction too, but they have grown on me a little bit due to the composability they provide. In the end I stopped using react altogether because the interop overhead annoyed me, having to translate CLJS datastructures to JS and back. It was never quite as performant as raw react, and react itself isn’t exactly fast. react is also written with JS in mind, so it only uses things that JS developers can reasonably express in code.
So I thought about what a pure CLJS implementation, using the power of macros, could look like and implemented that. I have been using that ever since. There is lots of left to do with that, but it works fine. I reached my performance goals and eliminated all npm/JS dependencies. Feels good to be pure CLJS, although that means losing access to the entire JS/react ecosystem and the available libraries. At times it can be annoying having to implement everything myself, so doing this is not something I’d recommend for everyone.
Started out with Reagent, ended up on Rum since it had support for frontend and backend. Haven’t ever used “plain” React (I did use Angular + typescript at a previous job though).
There wasn’t anything I couldn’t do. It was a pretty good setup really.
I gradually realized that the stuff I was building was really quite simple in terms of UI and that server-side rendering was good enough, so I switched to that. It was nice to eliminate a “layer,” and not having a compile step has also been kind of convenient. Did that for a year-ish and then starting sprinkling in some htmx which I’ve been very happy with as well.
That’s mostly in the context of my work previously as an entrepreneur. Now I have a regular job where we use Fulcro. It’s pretty good now that I’ve gotten the hang of it. Though still biding my time until there’s a good opportunity to introduce some htmx at work .
In case you haven’t seen it, I want to mention Replicant, which is relevant to the “pure CLJS React replacement” topic being discussed here. This project is hot in the Norwegian Clojure sub-community at the moment. It’s being developed primarily by @cjohansen, @slipset, and magnars. It can be considered a much faster successor to dumdom, which @maxweber mentioned above.
I picked up React when it came out. I was immediately charmed by its promise to treat all UI updates as if it was the first (e.g. “render the whole UI on every change”). I used it as a rendering library from JS, then shortly after played around with Ohm, and then landed on Quiescent, which I used for several years. Quiescent delivers fully on the functional top-down render - it hides component local state entirely.
Not really. My main gripe with React was the feature creep and random API changes. I got tired of spending time to do updates and upkeep just to keep my current feature set.
I created dumdom as an API compatible replacement for Quiescent. It uses snabbdom, which is a smaller JS vdom library. Unfortunately it too has gone through some churn (although less than React). I swapped our main app over to it and used it for several years in various projects.
Personally I consider disengaging with the npm ecosystem being a strength. That’s why I’m currently working on yet another library, as @Leif mentioned. The goal is to have easy to use, performant enough tooling that doesn’t depend on the churn of the JS ecosystem. Step one is a native CLJS virtual DOM library, then eventually I’m hoping we can build more stuff on top of it to make for a nice suite of tools.
@thheller I’m curious about your macro-driven solution. Is it available somewhere?
You’re sort of touching on it, but I’m curious as to where your need for speed comes from? Not saying this is the case for you (since you’re not “most developers”), but I have a feeling that most developers are somewhat mistakenly obsessed with rendering speed, when either the slowest, most naive thing will do, or they’re using the fastest thing ever hooked up to something that makes it slow.
Somewhat case in point. dumdom has been used in anger by @cjohansen for the longest time. With more than acceptable performance. But it turns out that when benchmarked, it’s really slow compared to the alternatives.
To me, the problem is not that dumdom is slow, but that most devs would not choose to use it on the account of being too slow, even without considering what fast enough is.
Various thoughts about different topics are on the doc folder. The DOM macro parts are outlined in this, a bit outdated but still mostly accurate. The entire shadow-cljs UI is implemented with it as well as a few non-public work things. The benchmark numbers I mentioned are here where the “Full” variant is using EQL queries/subscriptions and a normalized DB, whereas “Light” is just an atom. Not super optimized but good enough. I’ve been meaning to work on a few things. Your talk about your architecture stuff actually gave me a few ideas, but it is hard to find the time.
Good Question. I guess I’m annoyed that I have an inifitely fast Computer and yet still most Websites cannot even achieve 60fps, and often not even 5fps on my old tablet. It is true that for most things it doesn’t matter, but from a library/framework perspective you cannot add performance after the fact. If the baseline isn’t fast the rest can’t be either.
The benchmarks themselves are more of a game, since it is absolutely nonsense to render the same thing thousands of times per second. No human could ever perceive that, but it is useful to highlight bottlenecks. If you go over the benchmark history I linked above you can see how slow I started out and how many easy wins there were.
Fast enough is somewhat of a trap. If updating the DOM already takes 50ms then you basically have no time for your app to do actual work, let alone get smooth animations. We are also too used to fast computers/phones. My old tablet is a good indicator on how slow most things actually are, what takes 2ms on my desktop easily takes 100ms there, and that is noticeable. A good self test is running Chrome with 6x slowdown in the profiler, many websites just become unusable.
FWIW performance doesn’t mean things become unusable from a developer perspective. I personally consider writing (<< [:div "Hello World"]) instead of just [:div "Hello World"] a non-issue and nothing else changes. One is approaching native DOM speeds, the other is at least 10 times slower. Easy choice for me.
But as you said … for most things it absolutely does not matter and it would be fine on focusing on developer experience/convenience vs the user experience. I believe we can have both though, which is why I wrote the thing.
While I agree that these are visually very similar, I’d say that they are functionally different. The macro version presumably turns the hiccup into something opaque - an object, function/closure or something similar? While the other variant is data that can be manipulated at runtime, serialized, etc. It opens for some interesting use cases. While these are similar to author inline, they have quite different runtime characteristics.
I was curious if the performance difference was that big. I compared the js-framework-bench for shadow-grove and replicant, as representatives for “macros vs data”, and I see shadow-grove at ~1.6x pure DOM speeds, and replicant at ~2.2x. A noticable difference for sure, but not quite 10x. Reagent sits at ~3.3x on my machine.
That is true, but these are not mutually exclusive. You can “activate” the interpreted mode by requiring the namespace that implements the protocols for raw hiccup structures and just use hiccup. So, both variants can coexist and you can add other variants with even more specialized implementations as well. This is all based on the couple simple protocols as I outlined in the doc I linked above.
In your second point you mentioned the data structures get translated between CLJS and JS. I’ve often wondered how it felt to use some other language to write the JS needed for React. Like, at what point does it start to feel like “it’d just be better to write in JS?” Sounds like the translating of data structures is one such point.
Yes, if you have to do a lot of conversion that can become a bottleneck. I can’t say exactly when this becomes an issue, as even in reagent for the most part it is not a problem. There is a cost and at some point worth optimizing.
It is entirely possible to have CLJS emit the JS objects directly, but that can be clunky to use. So, often macro based approaches such as mine or UIX or so do it as part of compilation, which eliminates the overhead for the most part.
There are also a bunch of libraries that try reduce this problem, but they each come with their own set of trade-offs.
I’ve migrated from Om (standard) to React without wrappers, just Clojurescript interop with the Hicada compiler in the middle. I have several templates to quick start an app with such a front-end, available to all.
I was using Reagent + Re-frame. Still do but only for my work.
I didn’t like that React components / function knows nothing about what it renders, forcing the developer to repeat itself when the information is already in the render function. I also didn’t like to have to interop with JS libs.
I wrote my own framework based on signals, in CLJS. It does address all the problems mentioned above. It’s a still a WIP and not published yet.
For CSS it’s the same story. I didn’t like Tailwind v2, so I wrote my own lib in CLJ(S) to work better.