Frontend Development with Clojurescript, Replicant, Nexus and Datascript

Web app exercise implemented using Replicant, Nexus and Datascript. The right upper panel is showing Dataspex tool with Datascript database datoms.
Introduction
In my previous blog post Frontend Development with Clojurescript and Replicant I wrote about implementing a UI using the Replicant UI rendering library. In that previous exercise I built a custom dispatch system and used a Clojure Atom as my application store for storing the state of the frontend application. In this new exercise I use Nexus to do the heavy-lifting for creating the dispatch system, and instead of storing the application store in a Clojure Atom, I use Datascript. In the previous exercise I used the Gadget tool to visualize the data store. In this new exercise I use Christian Johansen’s new Dataspex tool.
The exercise I explain in this blog post is in my Clojure Github repo in directory replicant-nexus-datascript-webstore.
Only the frontend code is new / has been refactored to use Nexus and Datascript. The backend code and tooling is the same as in the previous exercise (just bumped the newest library versions for the backend as well).
Reference Material
I used replicant-state-datascript repo as an example how to setup Nexus + Datascript. Christian Johansen also provided me good help in the Clojurians Slack - Thanks! Christian also kindly reviewed the frontend source code related how I use Nexus, and the Blog post I wrote.
Links to the libraries:
Links to the example projects:
- replicant-state-datascript: An excellent example project which provides a skeleton for using Replicant + Nexus + Datascript. This example is all you need to get going.
The Libraries
A short introduction to the libraries. Read more about the libraries in the links provided above.
Replicant
Replicant is a data-driven rendering library for Clojure(Script). It renders hiccup to strings or DOM nodes.
I like the idea of Replicant. In the frontend world React dominates the scene at the moment, and there are a few React wrappers in the Clojure scene like Reagent and UIx. I have used Reagent and it is a good library to build React apps using Clojurescript. But when I was introduced to Replicant I realized that I hardly ever use the React ecosystem off-the-shelf components, and therefore I do not need to build the frontend using React. Replicant is a very good light-weight alternative to using React.
Nexus
In my previous Replicant exercise I used a custom built dispatch mechanism (that I mostly borrowed from others, see the source code for more information). This is just fine. But now that we have Nexus, why not let it handle the dispatching mechanism.
Datascript
Datascript is an immutable in-memory database and Datalog query engine in Clojure and ClojureScript. Therefore it is a good match to store the frontend application events in Datascript database, and using Datalog query language to query the application state for generating the UI.
Dataspex
In that previous Replicant exercise I used the Gadget extension to visualize my data in the Chrome developer tools. In this new exercise I used Dataspex. Dataspex is essentially Gadget 2.0 implementation.
Dataspex is a Chrome / Firefox extension that you can use to visualize the data store of your frontend application, i.e. the Datascript database in this exercise. Dataspex is just amazing. You can see all datoms generated by your frontend application in the Dataspex window, and drill down to a specific datom. See the picture in the beginning of this blog post for an example.
Frontend Initialization
I borrowed from the excellent replicant-state-datascript repo the solution how to register the datascript database for watching changes, and trigger Replicant/render. See app.cljs - init! function.
So, in the add-watch we create a triggering mechanism to watch changes in our Datascript database. And if there are changes, we create a view state (view-state) which has the application state for the Replicant to figure out what changes we need to make in the UI, and finally ask Replicant to render the UI ((r/render !el (f-views/view view-state))).
Nexus Actions and Effects
I mostly borrowed the skeleton for Nexus actions and effects from the excellent replicant-state-datascript example application.
Read more about the difference between Nexus Actions and Effects in the Nexus documentation. Just briefly the difference is that an effect is something that has a side-effect (e.g. making a http get or post, or changing the application store state), and an action just returns data, i.e. data related to other actions and effects to be executed by the dispatching mechanism.
An effect example:
(nxr/register-effect! :backend/post
(fn [_ system params]
(when goog.DEBUG (f-util/clog "effect :backend/post, params:" params))
(f-http/post system params)))
I.e.: make a http post with the parameters.
An action example:
(nxr/register-action! :route/new
(fn [state params]
(let [pg (:pg params)
page-id (:db/id (:app/page (ds/pull state '[{:app/page [:db/id]}] :app)))]
;; Navigate to new product page.
[[:db/transact [[:db/add page-id :page/navigated {:page :new, :pg pg}]]]])))
I.e.: Set the :page/navigated to :new (new product page) so that our view knows which page to render.
Nexus Interceptors
Nexus provides an easy way to add interceptors for effects and actions.
(def logger
{:id :logger
:before-effect
(fn [{:keys [effect] :as ctx}]
(f-util/clog "Before effect: " (pr-str effect))
ctx)
; ...
(nxr/register-interceptor! logger)
I learned this from Nexus Interceptor example.
I realized that I can utilize Nexus interceptors when clearing the new product form after navigating away from that page:
(def track-previous-page
{:id :track-previous-page
:before-action
(fn [{:keys [action state] :as ctx}]
(let [[action-name _params] action
;; Check if this is a route action
route-action? (and (keyword? action-name)
(string/starts-with? (str action-name) ":route/"))]
(if route-action?
(let [;; Get current page navigation
app-page (ds/pull state '[{:app/page [*]}] :app)
current-page (get-in app-page [:app/page :page/navigated])
page-id (get-in app-page [:app/page :db/id])]
;; Store current page as previous before navigating
(if (and current-page page-id)
(do
(ds/transact! !conn [[:db/add page-id :page/previous current-page]])
ctx)
ctx))
ctx)))})
(def reset-after-new-product-page
{:id :reset-after-new-product-page
:before-action
(fn [{:keys [action state] :as ctx}]
(let [[action-name _params] action
;; Check if this is a route action
route-action? (and (keyword? action-name)
(string/starts-with? (str action-name) ":route/"))]
(if route-action?
(let [;; Get previous page
app-page (ds/pull state '[{:app/page [*]}] :app)
previous-page (get-in app-page [:app/page :page/previous])
;; Check if we're leaving the new product page
leaving-new-page? (and previous-page
(= (:page previous-page) :new))]
(if leaving-new-page?
(do
(when goog.DEBUG (f-util/clog "Leaving new product page, clearing form data"))
;; Clear new product form data
(ds/transact! !conn [[:db/retract [:db/ident :product/validation-error] :error]
[:db/retract [:db/ident :product/new] :title]
[:db/retract [:db/ident :product/new] :author]
[:db/retract [:db/ident :product/new] :year]
[:db/retract [:db/ident :product/new] :country]
[:db/retract [:db/ident :product/new] :language]
[:db/retract [:db/ident :product/new] :price]
[:db/retract [:db/ident :product/new] :director]
[:db/retract [:db/ident :product/new] :genre]])
ctx)
ctx))
ctx)))})
Some Development Tricks Used in This Exercise
Here I document some development tricks that are not related to the libraries but more generic Clojure or Calva related development tricks I want to remember in the future.
Move Calva Output to a New Window
Once you have connected Calva to your REPL, you get the output in the terminal / Calva Output. I like to move this window to another monitor (I have four monitors at my table) to see the Calva output window and VSCode editor at the same time (but in different monitors). Use VSCode command: Terminal: Move Terminal into New Window.
Browser Developer Tool Console Logging
I have quite a lot Browser Console logging in the code base like this:
(when goog.DEBUG (f-util/clog "register-action! :route/new, params:" params))
The reason is that this is an exercise, I was curious to see what kind of parameters there is in those functions while developing the functions. I left the console logging intentionally there if someone uses this exercise as an example to learn how to use these libraries.
Other Development Tricks
You can read about other development tricks from the previous Replicant exercise - they are mostly the same in this exercise (using Babashka…).
Conclusions
Replicant provides an excellent light alternative to mainstream heavy UI frameworks like React. And now with Nexus and Dataspex developing UI with Replicant is really enjoyable.
The writer is working at a major international IT corporation building cloud infrastructures and implementing applications on top of those infrastructures.
Kari Marttila
Kari Marttila’s Home Page in LinkedIn: https://www.linkedin.com/in/karimarttila/
