Clojurescript with NPM, React and Reagent in 2021
Intro
There is much outdated information floating around on the web regarding the use and integration of npm modules with clojurescript. Some
- focus on shadow-cljs (and here), which is not an option for many clojure/clojurescript-developers who use the easier and more flexible figwheel,
- use the :foreign-libs option a concept that has been alpha for a number of years,
- using :npm-deps, which still requires messy, hard to create :externs files, with the cljsjs project that was largely about externs sharing, largely abandoned.
These documents come up with every web search for "clojurescript npm" etc. and lead to a huge amount of confusion, hurting the progress and adoption of clojurescript and clojure. NPM integration is essential and these approaches are dead ends.
The situation in 2021
Things has become much better, the intgration much easier, with the introduction of packagers like webpack and parcel into the already fantastic clojurescript stack. This guide to webpack integration with clojurescript is well worth reading: https://clojurescript.org/guides/webpack
A Youtube video is an excellent summary of the approach and I wanted to put the gist of this video, the code snippets, on the web as text:
Figwheel Main
Figwheel Main is an indispensible, mature too for clojurescript development. shadow-cljs should have similar facilities but I had all kinds of problems integrating this tool into my workflow.
With Figwheel Main installation, the main issues usually arise from a Jetty version mismatch. So it is advisable to use the Jetty version used by Figwheel Main, currently 9.4.36.v20210114. You should exclude all other Jetty version with a :exclusions statement in your build file. I also use ring-jetty9-adapter for http2 support. This has to be kept at version 0.14.3 for Jetty versions under 10, otherwise you will get compilation errors due to incompatability!
In the figwheel documentation, first read https://figwheel.org/docs/npm.html. Then your create a fighweel config file in the root of your project, named sth. like
dev.cljs.edn
for the "dev" build.
In this file you simply add the figwheel config option like so for example:
^{:auto-bundle :webpack
:watch-dirs ["src"]
:css-dirs ["resources/public/css"]
}
This setting does a lot. It sets the compile :target to :bundle and puts the correct webpack command in the pipeline as the final step. So the final compilation is not by the closure compiler as usual, but webpack.
Don't forget to update the path of your js include file in your html to the result of this last compilation and bundling, which is usually something like "/cljs-out/dev/main_bundle.js" while with regular figwheel it's sth. like "/cljs-out/dev-main.js" (epending on your config of course).
Node Stuff
Initialize npm in your project folder:
npm install -y
This will do a bunch of stuff, among the create a package.json file. This file we don't have to care about any further. No need to deal with the mess that is node and JavaScript from our beautiful, organized Clojure world!
Then install webpack:
npm add --save-dev webpack webpack-cli
Install the npm package you want to use, for me it was react-chords:
npm install @tombatossals/react-chords
If you use reagent, also install react and react dom here, because cljsjs/react and cljsjs/react-dom will NOT WORK with webpack (see below).
npm install react react-dom
Now there are react and react-dom folders in the node_modules folder. These can be used by webpack.
For some reason, I also had to npm install acorn, no idea why but it wasn't a problem.
In your cljs
In your cljs file, require your npm package, due to the package name in this case it has to be a string, in most cases it's just a symbol just like regular cljs:
(ns your-ns
(:require
[reagent.core :refer [adapt-react-class]]
[reagent.dom :as rdom]
["@tombatossals/react-chords/lib/Chord" :as Chord]);;<-- here's your npm, Chord has has the js class/function
)
In node.js the import code is
import Chord from '@tombatossals/react-chords/lib/Chord'
and then the react usage is to just return a
<Chord chord={chord} instrument={instrument} lite={lite}/>
from some function.
In clojurescript I could get a reagent component with this process:
- Make a require like above, no :refer, just :as.
- The Chord symbol referenced with :as now contains #js, obviously a JavaScript object wrapped in a little data structure.
So to retrieve it
(def chord (get (js->clj Chord) "default"));; there may be simpler ways to do this
and adapt it for reagent (as with cljsjs components):
(def MyChord
(reagent/adapt-react-class chord)
)
MyChord can now be used as a normal reagent tag:
(rdom/render
[MyChord {:chord chord-config-hash
:instrument instrument-config-hash
:lite lite}]
(.getElementById js/document "chord_container"))
Advanced compilation for production
In Clojurescript development, the bugbear is advanced compilation. Everything might work fine in development mode, when :optimizations is :none, but things fall apart when :optimizations is :simple or :advanced. With npm integration and react, after a successful compilation and webpack run, you may get cryptic in Chrome dev tools errors like:
main_bundle.js:2 Uncaught TypeError: Cannot read properties of undefined (reading 'unstable_cancelCallback')
at main_bundle.js:2
at Object.444 (main_bundle.js:2)
at __webpack_require__ (main_bundle.js:2)
at main_bundle.js:2
at main_bundle.js:2
This error is in a huge single-line document, with all cryptic variable names. Impossible to debug or even to load into emacs.
Again it's advisable to slowly and thoroughly read the npm guide at figwheel.org, especially the "Compiling for production section." Then add :clean-outputs to your figwheel config meta in <your production build>.cljs.edn:
^{:auto-bundle :webpack
:watch-dirs ["src"]
:css-dirs ["resources/public/css"]
:clean-outputs true ;;<-- important!
}
Solution
The cause for above error is hinted at here: https://clojureverse.org/t/clojurescript-fulcro-and-advanced-compilation/7355 and this discussion might be interesting.
Basically, React is not required correcly by webpack, thus undefined and all vars based on it likewise. This happens when there's cljsjs/react and/or cljsjs/react-dom in the source folder of webpack (default: /target/public/cljs-out/<your build name>).
Apparently, webpack will get confused and use neither this package nor the one at <project_root>/node_modules/react as is preferable and even expected by npm modules that rely on react.
You now have to exclude all references to cljsjs/react and cljsjs/react-dom via :exclusions statements, for leiningen in your project file. It's usually not enough to just use
[reagent "1.1.0" :exclusions [cljsjs/react cljsjs/react-dom]]
(here's how to do this with Clojure CLI: https://clojurescript.org/guides/webpack) as is sometimes suggested in other articles.
It's much better to make the :exclusions at your defproject level that do this (leiningen docs):
"Global exclusions are applied across the board, as an alternative to duplication for multiple dependencies with the same excluded libraries."
See the leiningen sample project.clj:
(defproject com.myproj "0.1.0-SNAPSHOT"
:description "My great project."
:dependencies [
[org.clojure/clojure "1.10.3"]
[reagent "1.1.0"]
...
]
:exclusions [cljsjs/react cljsjs/react-dom cljsjs/react-dom-server]
)
In leiningen, you can now run
lein deps :tree
until you're absolutely sure you have no reference to these libraries. You may also check target/public/cljs-out/prod if there is a cljsjs folder with these libs.
Then run your usual build production build command, for lein, long and short:
lein run -m figwheel.main --optimizations advanced --build-once prod
lein fig -- -O advanced -bo prod
It should work now, at least not throw the above error.
Final thoughts
There you have it, an easy integration of npm and clojurescript, just one line in your figwheel config, just two npm packages to install, a single :exclusions in your project file, no messy :foreign-libs, :npm-deps that never worked, no externs nightmare, no abandoned cljsjs projects. The clojure/clojurescript stack is really approaching perfection and the npm integration is absolutely crucial. It now works very well and will only get better.
Hope it helps.
Posted: 18 October 2021