clj-commons / citrus

State management library for Rum
Eclipse Public License 1.0
274 stars 21 forks source link

Modifying the state backend-side #42

Open jacomyal opened 5 years ago

jacomyal commented 5 years ago

In my current project, I am trying to implement a no-JS fallback to the most basic state controllers (ie. the ones that only emit :state). It would work like this:

  1. The user clicks on a button, which is actually the submit of a hidden form
  2. The browser calls the current URL as a POST call, with the controller name, the event name and the serialized params in the payload
  3. The backend instanciates the reconciler, applies the related state controller (that would modify the related state branch), then renders the HTML page and sends it back

But I have two issues:


Why I cannot modify the state backend side

The current implementation of the Resolver is written such that it will always resolve the state branch with the initially given resolver function:

;; ...
clojure.lang.IDeref
(deref [_]
  (let [[key & path] path
        resolve (get resolver key)
        data (resolve)]
    (when state
      (swap! state assoc key data))
    (if reducer
      (reducer (get-in data path))
      (get-in data path))))
;; ...

By the way, this is also clearly stated in the doc.

There is no way to bypass that, except by instanciating a new Reconciler, with resolvers that return the modified state branches - which looks definitely smelly.

Can you see any other way to do that, with the current implementation of Citrus?

Also, a solution would be to first check if the related state branch is already in the state, before calling the resolver:

;; ...
clojure.lang.IDeref
(deref [_]
  (let [[key & path] path
        resolve (get resolver key)
        data (if (contains? state key)
               (get state key)
               (resolve))]
    (when state
      (swap! state assoc key data))
    (if reducer
      (reducer (get-in data path))
      (get-in data path))))
;; ...

Which by the way would deal with caching issue at the same time, and allow me to transform the state by just reset!ing the state atom.


Why using resolvers at all

I understand that the purpose of resolvers is to only load data that will actually be used in the UI. But the way I see it, I think it's not the best design:

So the code can become quite verbose, to have something that is not necessarily done the best possible way.

Meanwhile, if Citrus would simply skip this feature:

The state would no more be lazy, which would make manipulating it way easier. What do you think about it?

jacomyal commented 5 years ago

Quick follow-up, I finally use this code (which still seems a bit verbose, just to update a bit of data):

(defn reduce-reconciler
  [reconciler {:keys [controller-name event-name params]}]
  (let [resolver   (-> reconciler :resolvers controller-name)
        data       (if (fn? resolver) (resolver))
        controller (get controllers-map controller-name)
        result     (if controller
                     (controller event-name params data))]
    (if (contains? result :state)
      (citrus/reconciler {:state     (:state reconciler)
                          :resolvers (assoc (:resolvers reconciler) controller-name (constantly (:state result)))})
      reconciler)))