I love Clojure and ClojureScript so I tried to rewrite a complex React component we have at work from TypeScript to cljs & Fulcro. The lacking tooling and painful interop made it not worth the effort. I’d like to hear from others, how they deal with this.
The first problem I encountered was tooling. I needed to use many existing TypeScript components. In TS, I get auto-completion for their props with information about the expected type and I can click through to their source code to learn more about them. This is indispensable. Especially since their documentation is somewhat lacking. There is no such thing for these components when using them from ClojureScript. (In Calva, but I imagine it is the same everywhere.) This is a big reason to prefer writing my components in TS.
I imagine that having types or prop types on components I write is also quite helpful to their users (which might include future me), if they are non-trivial.
Another problem I had was that interop is somewhat painful. Especially in this case, where I had to provide various callbacks and child components / factories to the TS component. Once the props pass to this TS component, they become javascript objects and that is not very convenient or nice to work with. (Though I could perhaps “tunnel through” clj data by putting it as-is inside a js prop: #js {:jsProp my-cljs-data}.) Here are a few examples:
(tsComponent
{:getRowComponent #(if (.-isAggregateRow ^js %) AggregateRow Row) #_...}
;; the TS code uses styled `tr` and somehow that does not complain about the extra props but my
;; code does so I have to remove them from the props before passing them on:
(defn Row [props] (dom/tr (doto (gobj/clone props)
(gobj/remove "isSelected")
(gobj/remove "dragDropProps")
(gobj/remove "dataSourceRow"))))
(defn AggregateRow [props] (dom/tr #js {:style #js {:background "black" :text "white"}}
(gobj/get props "children")))
It is even more complicated because the existing codebase uses styled-components
so the original TS implementation can do
So, you have TS already, but played with the idea of rewriting it in CLJS? I think the idiom of all Clojure is, stick with what works; Clojure has always placed a high value on being pragmatic.
I don’t know much about Fulcro, but I would be tempted to at least get the syntax benefits of Reagent (Hiccup) in an attempted re-write. I could be mistaken, but my understanding is that the opinionated nature of Fulcro is unlikely to co-exist peacefully with a re-write of TypeScript (which is also somewhat opinionated, by nature).
As I’ve heard said on many other “Clojure vs Other” answers, you can’t just rewrite their code and their strategies in Clojure and hope in improve; Clojure invites different ways of solving some of the problems.
The interop, though, is a different thing. Sometimes you can’t and shouldn’t bypass all of that – it would be reinventing the wheel. But from your example I again wonder if Fulcro adds a layer of complexity to interop?
Do not misunderstand: I do not intend to actually rewrite the TS application. I only attempted the (partial) rewrite as a learning experience.
I do not see any syntax benefits to Hiccup. I am quite comfortable with calling functions. Fulcro is opinionated regarding data management - and it is what I am after at this experiment.
Regarding interop - there is nothing much Fulcro-specific in my snippet. I do not think it would be any different with (defn Row [props] [:tr ..])
Maybe a but tangential, but this reminds me about that it is a bit of a struggle writing Joyride scripts having to do with the lookup/tooling. My gut tells me we should be able to fix this. And maybe even improve on the tooling experience you have with TS code. Wrote some about it on Joyride Discussion a while ago What if npm and vscode documentation lookup was as awesome as it is in Clojure?
I’m a big fan of those sort of learning excursions, and go down them myself often. I can see how it might be a good way to play with Fulcro.
I have an over-due essay to write about syntax, but I am a big fan of hiccup-style improvement on XML. One of the low-hanging benefits that might not be present with Fulcro is the compatibility with all core functions, since you are just using vectors and hashmaps and keywords so play very nicely with things like into, map, etc. Data-driven programming for the win! (consistent with Reitit, Garden, HoneySQL, etc)
Interop can be a tough nut to crack, though. My current apps don’t rely too heavily on it, but I have done some d3js (JS) and OpenCV (Java) that took some real figuring out.
The first problem I encountered was tooling. I needed to use many existing TypeScript components. In TS, I get auto-completion for their props with information about the expected type and I can click through to their source code to learn more about them. This is indispensable. Especially since their documentation is somewhat lacking. There is no such thing for these components when using them from ClojureScript. (In Calva, but I imagine it is the same everywhere.)
I don’t think it’s the same everywhere. I’m using Neovim, Conjure, nREPL and Cider in my projects and I get autocomplete, jump to source and all of that stuff, mostly out of the box (minues installing the plugins + adding aliases to my clojure cli setup). I’m not sure if Calva is supposed to support it, but might be worth looking into it or alternatively look into alternative tooling.
I don’t think there is any integration between nrepl/cider and TS specifically, and I’m not using TypeScript components myself in any project. But by default, this is what I see as autocomplete when writing Reagent components:
But yeah, you wouldn’t get autocomplete for anything from TypeScript, and not if there isn’t a direct connection between what you’re writing and how it’s being used, like passing props via the “magic” that :> introduces.
What I’d do if I’m using outside components, would be to wrap them in a proper Reagent component. Mostly for ease of use generally (not mixing “normal” Reagent code with :>, not having to use #js or clj->js all over the place), but in that case you’ll also get the autocomplete as you wish.
When it comes to types in ClojureScript, you have options like clojure.spec, Malli and others which arguably, provides a better experience than static typing when you’re dealing with interactive development.
Still, if you’re mixing a lot of TS and ClojureScript, you won’t get a lot of support from ClojureScript tooling when it comes to the TS code. You can’t look up docstrings (TS doesn’t have that AFAIK), types have already been compiled away, and so on.
So if most of your project will depend on TS code already, it’s probably not worth adding ClojureScript on top of that, unless you’re willing to wrap it in ClojureScript code in order to get the experience you’re after.
At the company I work for, we use TypeScript for our pure (or mostly pure) components along with Storybook, and we use ClojureScript (and re-frame) for the app logic. This has worked well for us. We actually started with just JS for the components, but an advisor advised us to use TypeScript, so we switched, and have been loving it.
We build components with Storybook from small ones like text inputs all the way up to app-specific screens, so the seam between ClojureScript and Typescript is at the screen level only. So most of the usage of components is really by higher order components and all of that is in TypeScript, giving us the benefits you mention for most of our component development.
I think the downside you mentioned is also related to how the components are designed. Since our screen components basically just take functions and data for their props (not other components), we don’t run into the issue you mentioned regarding passing child components. For what it’s worth, this is a React Native app, but we are starting to build a web app now and are going with the same approach, though we haven’t gotten far enough into yet to know for sure that it’ll work well there too. I suspect it will, though.
No. This is at code time, not runtime. And it’s props you should be creating and providing to the component so even in runtime they don’t exist until you make them
I’ve been working in TypeScript for almost 2 years now, personally I can’t stand it and would kill for an opportunity to work in cljs again. I appreciate some of the TS tooling, but I’d so much rather have a REPL to really test everything as I write it, and I miss the creative freedom I felt with ClojureScript. Always feels like my TS comes out far more complex and over-specified and while I get fast feedback if anything breaks, it still eats up time restructuring the types especially once a part starts incorporating types derived from other types and upstream type changes require updates in each downstream consumer in a component hierarchy. That said, given the nature of my current job, I feel cljs would be a much needed speed boost and encourage a better team culture over time but I can’t speak for anyone else’s work out there. For example, it’s not uncommon for a team member to spend a day or so refactoring types for performance. Not runtime performance, type performance for our editors. I’m glad to have someone with the expertise to improve on the DX like that, but I’d much rather we invest our time focusing on making the product quality better from a user’s perspective than have to be concerned with problems like that.
Though I wouldn’t mind more intelligence in the tooling and a smarter autocomplete. It is a really tough sell to new users\higher ups coming from TS without that sense of guidance it can give you.
Something that’s been on my mind a lot has been the idea of writing a new cljs compiler in Go like SWE, esbuild, and projects like that which run crazy fast. Never tackled anything like that before so would have to learn from scratch, plus I’m sure it would be a big undertaking but smoothing out the bundling experience and removing the dependency on Google Closure would be pretty worth it. I particularly like the way ReScript works where it transpiles ReScript into decently human readable JS and TS to be committed with the project. That way the transpiling is done during dev time and it can be added to a project without ever having to change any build settings to get it in production.
it’s becoming hard these days to avoid TypeScript… as most developers have decided it was now way to build apps… and even if we know they’re wrong it’s too late to convince them to change
but also sometimes, when i’m struggling with some js interop with cljs, or when i find myself using js->clj or clj->js a bit too much… then sometimes i wonder: am i wrong? should i be using typescript for my apps instead of the cljs stack i’ve been using for the last 7 years?
and then i look at my Clojure code, at my re-frame events and reagent components, and i relish this “economy of expression”
i don’t understand how TypeScript won this battle (this war?), as it doesn’t make apps more robust and doesn’t fix anyway the main issue with js which is imo the “npm hell”
anyway sorry i’m just rambling here, as a grumpy dev
–
your idea of a cljs compiler in Go sounds interesting!