Making my way through `shadow-cljs` and `reagent` - asking for opinion

clojurescript

#1

Dear community,

I’ve recently posted few questions on this forum and received kind answers. Thank you for that! What I wanted to ask you right now is some kind of „early-stage diagnosis” for anything especially wrong with how I understand shadow-cljs and reagent should be used together in order to help me catch any bad behaviors (and I do expect them, as I am completely new to both ClojureScript and Reagent, although having experience in other Lisps).

That’s the sample page with shadow-cljs, reagent, secretary and accountant.

(Of course the content is completely dumb placeholder, I was just lurking around)

I am not convinced if request like that is appropriate here or not - I really apologize if this is going to be unkind or against some rules. If not - I am waiting for all the criticism at this very first step.

Thank you!


#2

shadow-cljs side of things looks good. I’m not a reagent expert either but looks good to me too.


#3

Don’t think there is right or wrong. But most of the reagent apps will use one big atom for app-state, and use cond or defmulti for routing?

one big atom is because “single source of truth” is simpler model, the ui is just a reflection of the data structure. the only exception is the component internal state (e.g. animation?).

the use of cond or defmulti seems easier to reason, since the ui component will not get mixed up with the routings.


#4

So we are talking about two separate problems:

  1. Using one system (referencing to reloaded workflow) or app-state atom as a map instead of multiple ones. In my case this is counter atom and some-list atom for two separate pages.
  2. Routing is relying on „mutable” state of one content atom.

ad 1)

I am not convinced here - with separate atoms updates might end up more complicated because I will have to modify only one key of app-state, like:

(defn increase-counter
  [state]
  (merge state {:counter (inc (:counter state))}))

;; on-click
(swap! app-state increase-counter)

Or shorter

;; on-click
(swap! app-state #(merge % {:counter (inc (:counter %))}))

Instead of just:

(swap! counter-state inc)

Which one is better? I don’t know! I like the idea of passing app-state atom as a page argument to keep it free (sort of) of hardcoded side effects. But as of writing this, I am starting to be more towards using this little merge and be able to keep state in one place (maybe this will be useful with some Redux implementation? I haven’t explored this area in CLJS yet).

ad 2)

What I don’t like in my approach is keeping reagent component inside application state, and I think that’s the main flaw. Maybe I should do it more like, following reagent-cookbook:

(ns test.main
  (:require ["react-bootstrap" :as bs]
            [reagent.core :as r]
            [secretary.core :as secretary]
            [test.menu :as menu]
            [test.about :as about]
            [test.counter :as counter]))

(def app-state (r/atom {}))

(defmulti current-page #(@app-state :link-key))

(defn- bold-if-active
  "Depending on `LINK-KEY` being marked as currently active, return `TEXT` with `BOLD` tag around"
  [link-key text]
  (assert (keyword? link-key))
  (if (= (:link-key @app-state) link-key)
    [:b text]
    text))


(secretary/defroute counter-page "/counter" []
  (swap! system/state #(merge % {:link-key :counter})))

(defmethod current-page :counter []
  [counter/page])

(secretary/defroute about-page "/about" []
  (swap! system/state #(merge % {:link-key :about})))

(defmethod current-page :about []
  [about/page])

(...)

(defn page []
  [:> bs/Grid
(...)
      [:> bs/Nav
       [:> bs/NavItem {:event-key 1 :href "/about"} [bold-if-active :about "About"]]
       [:> bs/NavItem {:event-key 1 :href "/counter"} [bold-if-active :counter "Counter"]]]]]]
   [:> bs/Row {:class-name "show-grid"}
    [:> bs/Col {:md 12}
     [current-page]]]])

I like this because it felt like content and link-key were redundant (keeping what is rendered together with rendered content).

Did I understood your advice correctly, that’s what you meant?

Thanks!


#5
  1. you can think the big atom as a clientside database, it has nothing to compare with system. you can also consider the app-state as a component in system

  2. Clojure have many tools for this kind of tasks

e.g.

(swap! app-state update :counter inc)
; or
(swap! app-state update-in [:counter] inc)

#6

Oh that’s nice, I didn’t knew about update!

For me system in reloaded workflow is sort of app-global database describing current state of application. Resetting it to default should be, in essence, equal to restart-and-reload.

Also, I’ve submitted my previous reply too early by mistake and later edited it, so now it’s complete.


#7

Looks good. Reagent by itself is pretty agnostic about using a single ratom or multiple smaller ones. Personally I sometimes start with multiple ratoms while exploring, then refactor to collect all state in a single !app-state var (I use the leading exclamation mark to mean “atom-like var”). Reasons why I think it’s often a good idea:

  • Unification: When there are bugs, you know where to look, in contrast to state being spread out across multiple places around your app.
  • Tooling: You can use tools like data-frisk or re-frame-trace to inspect the data in a well known location (or just (prn @!app-state) from the REPL)
  • Greppability: Every line of code that affects state has the word !app-state in it.

Finally, once you think it’s time to regulate state updates more strictly, you can smoothly move on to re-frame or one of the other state-management frameworks for Reagent.


#8

Yes, I consider system as a tool for handling stateful components of a system which have the needs to take extra care of lifecycle. However, the data of the application is not under this category. For example, if you are building a todo app, where to read/store your todos? you will need a database/datastore for storing your app data. system will be used to manage your connection/reference to the database/datastore, but data itself should not be managed by system.

your example are pretty much close to finish, the parts left are

how to organise/manipulate you data/data model,
how to handle side effects/async stuffs (ajax, scheduling rerender, local storage…etc),
how to handle quirks/performance.


#9

This is becoming blurry, but I think that database connection is system, actually displayed page also is system but list of todo items or state of the coutner is not system. For me, dividing line is between runtime state and data state.

I’ve incorporated your changes into https://github.com/otwieracz/reagent-test/commit/fa58fb2beddfcb3d79573b27f7c40d075a993cbe

Do you have any other suggestions?

Thanks,
Slawek


#10

your commit look good.

A really good article for you to understand more about reagent.

For system topic, I don’t have much to provide :man_shrugging:.