Search code examples
clojurescriptre-frame

How to create a custom "getter" query in cljs re-frame?


I'm writing a re-frame app that models a board game where I have a board structure containing an array of cells, something like:

{... :board-cells [{:name "cell-1" :material #object} {:name "cell-2" :material #object} ...]}

While re-frame supports the getting of "natural" substructures with a nice keyword syntax like (db :board-cells), I'm getting tired of having to write the entire "drill-down" query every time I want to get a material: (get (nth (db :board-cells) index) :material). This also has the downside of tightly coupling the physical layout of my db to my application logic. What if I decide to change my db structure? Then I have to update ten different spots instead of just one.

Is there a re-frame official way to create a "virtual query" so I can get a material with something like (db :get-nth-mat n), where n is the cell number within the board-cells array? I thought that db.cljs and reg-sub was where I could do this, but it doesn't seem to work. Yes, I can create my own getter:

(defn get-material [db index]
  (get (nth (db :board-cells) index) :material))

and call it like (println "mat-4=" (cell/get-material db 4)), but this isn't as convenient or nice as (db :get-nth-mat n)

Many thanks.


Solution

  • db is just a map and this "feature" there has nothing to do with re-frame, but every map is a function and so are keywords. So when you do (map something) or (:keyword something) you are actually doing (get map something) and (get something :keyword).

    So there really is no "shortcut" other than accessing/iterating your data differently (e.g. doseq, for, map, ...) - assuming you are about to render the grid cell by cell; This way you get rid of the index based access at all.

    Otherwise I'd use a dedicated function like yours, but would rather name it material-by-idx (it's rather uncommon to name function get and set like accessors in OO (but there are places for it like e.g. set for state modification)).

    Having properly named, ideally pure, functions, that do one thing properly is an important building block in functional and Lisp programming. And often the downside of having to type a bit more can be mitigated by higher level programming paradigms like threading or partial application or as last resort, macros.

    And you can use get-in to unclutter it a bit:

    (defn material-by-idx [db idx]
      (get-in db [:board-cells idx :material]))
    

    E.g. you could now in your loop use something like this, if you see value in that:

    (let [mat-at (partial material-by-idx db)]
      (mat-at 5))
    

    Btw: The version you are wishing for (db :get-nth-mat n) actually works (but not as you wish for). It turns into (get db :get-nth-mat n) (3 argument get), which returns you n if there is no key :get-nth-mat in db.