Skip to content

Quick Start

zalky edited this page Apr 30, 2023 · 8 revisions

The first thing to note is that the three Re-frame event handlers reg-event-db, reg-event-fx, and reg-event-ctx have been re-bound in reflet.core and modified to support multi-model (graph and non-graph) db operations. The Reflet handlers are drop-in replacements for the Re-frame ones. If you prefer, you can use them selectively, but it is generally easier to just use the Reflet handlers everywhere in your app to avoid confusion.

Additionally, reflet.core has short-hands for three of the most commonly used Re-frame view functions:

  • subscribe -> sub
  • dispatch -> disp
  • dispatch-sync -> disp-sync

These are used throughout the documentation and the Reflet example client.

Configuration

At minimum you need to dispatch the Reflet configuration event before any graph mutations occur, or any graph pull subscriptions are created.

(require '[reflet.core :as f])

(f/disp-sync [::f/config])

See the Configuration document for examples and other options, like how to enable the debugger.

Quick Start

The entry point to Reflet is the with-ref macro:

(require '[reflet.core :as f]
         '[reflet.interop :as i])

(defn widget
  [props]
  (f/with-ref {:cmp/uuid [widget/self]
               :js/uuid  [widget/obj]
               :el/uuid  [widget/el]
               :in       props}
    (let [state    @(f/sub [::graph-query self])
          on-click #(f/disp [::toggle self])]
      [:div {:ref (i/el! el)}
       [:button {:on-click on-click} "Toggle"]
       (when (:widget/open? state)
         ...)])))

The with-ref macro generates unique references that can be used as ids or names almost anywhere in your Re-frame application. This instance generates three references, and injects them :in the props map at the :widget/self, :widget/obj, and :widget/el attributes. It also exposes local bindings to the references. The semantics are almost the inverse of Clojure map destructuring.

The references will look something like:

(vals props)
=>
[[:cmp/uuid #uuid "bf8cc02d-271d-4779-843c-e3829f800cb6"]
 [:js/uuid #uuid "3e57d7a2-9148-47b0-81b8-950ed11f74d0"]
 [:el/uuid #uuid "8f4549c8-20de-4c37-a4ec-6072b40e512f"]]

Both the unique attribute and the value type are configurable. There's a more detailed account of the with-ref macro in the References and Application Design document.

But once you have your references you can use them anywhere in your application, and with the other features of Reflet. Above, widget passes the self reference to both the ::graph-query subscription and the dispatched ::toggle event.

Event Handlers

In the ::toggle event handler, the self reference is then used to update the associated graph entity in the db:

(require '[reflet.core :as f]
         '[reflet.db :as db])

(f/reg-event-db ::toggle
  (fn [db [_ self]]
    (db/update-inn db [self :widget/open?] not)))

Specifically, this models some component local state for our widget.

Besides supporting multi-model db operations, everything about the Reflet event handlers is the same as the Re-frame ones. They are drop-in replacements. Interceptors, effects, and coeffects all behave the same, and the db coeffect is the same old Clojurescript map. Also all the Reflet graph mutation functions, like update-inn, are functionally pure. Therefore, you can freely interleave regular associative operations with graph ones:

(f/reg-event-db ::toggle
  (fn [db [_ self]]
    (-> db
        (db/update-inn [self :widget/open?] not)
        (assoc ::flag true))))

This makes it really easy to update existing Re-frame handlers to support graph mutations. The Multi Model DB document describes the mutation API in more detail.

Queries

On the query side, we define a ::graph-query pull subscription to return data from the graph entity referenced by self:

(f/reg-pull ::graph-query
  (fn [self]
    [[:widget/open?
      :widget/playing?
      {:widget/selected-items [:item/id
                               :item/description
                               :item/position]}]
     self]))

reg-pull is described in more detail in the Graph Queries document, but the result of the query is returned in denormalized (tree-like, rather than graph) form.

In the component we subscribe to this result just like any other subscription:

(defn widget
  [props]
  (f/with-ref {:cmp/uuid [widget/self]
               :js/uuid  [widget/obj]
               :el/uuid  [widget/el]
               :in       props}
    (let [{:widget/keys [open? selected-items]
           :as          state} @(f/sub [::graph-query self])]
      [:div
       (when open?
         (doall
          (for [{:item/keys [id description position]
                 :as        item} selected-items]
            ...)))])))

As with events, all the normal subscription semantics (like caching and disposal) still apply. Regular Re-frame subscriptions that query non-graph data still work just like before, and they can be combined with graph subscriptions anywhere in layer-3 Re-frame subs:

(require '[reflet.core :as f])

(f/reg-sub ::non-graph-query
  (fn [db _]
    (get db ::flag)))

(f/reg-sub ::layer-3-sub
  (fn [[_ self]]
    [(f/sub [::graph-query self])
     (f/sub [::non-graph-query])])
  (fn [[denormalized-state flag] _]
    (combine denormalized-state flag)))

Thus, given an existing Re-frame application, Reflet can be integrated with minimal, incremental effort, and deployed where the graph data model would bring the most benefit.

Beyond the graph data model, Reflet provides some additional tools to help with application design.

Finite State Machines

Sometimes the imperative implementation in event handlers is more easily described by a declarative FSM spec:

(require '[reflet.fsm :as fsm])

(fsm/reg-fsm ::widget-fsm
  (fn [self other-fsm]
    {:ref  self
     :attr :my.state/attr
     :stop ::done
     :fsm  {nil       {[::toggle self] ::started}
            ::started {[::edit self] ::editing}
            ::editing {[::reset self] ::started
                       [::done self]  {:to       ::done
                                       :dispatch [::complete other-fsm]}}}}))

Here, we've defined an FSM that advances the entity referenced by self through a set of state transitions. The transitions occur whenever an event, like [::toggle self], fires. Normally this behaviour would be defined ad-hoc in a handful of Re-frame event handlers, with some imperative logic around when and where to transition.

You can then start the FSM and subscribe to its state in your view components:

(defn widget
  [{:keys [other-fsm]
    :as   props}]
  (f/with-ref {:cmp/uuid [widget/self]
               :js/uuid  [widget/obj]
               :el/uuid  [widget/el]
               :in       props}
    (let [fsm-state @(f/sub [::widget-fsm self other-fsm])]
      ...)))

The parameters self and other-fsm are references generated by with-ref. All FSMs live as graph data in the db, and their states or participating events can be used as inputs to other FSMs to produce more complex, hierarchical behaviours. You can find more on FSMs in the Finite State Machines document.

Mutable State

In addition to FSMs, stateful JS interop or DOM manipulations can be made simpler by using references to access mutable state. Recall that besides the :widget/self reference, the with-ref in our widget component created two other references: :widget/obj and :widget/el.

These can be used in interop methods to register and retrieve mutable state:

(require '["js-library" :as constructor])

(defn create-js-obj
  "Creates and registers a JS Object."
  [{el-ref  :widget/el                       ; Get our DOM element reference from props
    obj-ref :widget/obj}]                    ; Get our JS object reference from props
  (let [config #js {:config :opt}
        dom-el (i/grab el-ref)               ; Grab the DOM element
        obj    (constructor config dom-el)]  ; Construct the JS object
    (i/reg obj-ref obj)))                    ; Register the JS object for later use

;; elsewhere
(defn update-js-obj
  "JS Object is retrieved and updated."
  [{obj-ref :widget/obj                      ; Get our JS object reference from props
    data    :widget/data}]
  (when some-condition
    (let [^js obj (i/grab obj-ref)]          ; Grab the JS object
      (.updateMethod obj data)
      ...)))

This and all the other features covered in this Quick Start are described in more detail in the feature documentation. But it is the humble reference that connects all the facets of your application together.

Application Design

This pays dividends not just in the esoteric sense of a "unified approach", but specifically because of the way that with-ref allows parent contexts to transparently extend components with respect to:

  1. Resource access
  2. Lifecycle management

These two things are what typically drive complexity in large apps, and with-ref provides a great story for both.

At a high level the conversation between with-refs in the component hierarchy goes something like this:

  1. Child component with-ref: "I declare references A, B, and C. This is everything I need to function. I'm in charge of A, B, and C and will manage lifecycle behaviours, like cleanup."

  2. Then a parent with-ref says to the child: "No, I declare the A reference, so I can extend some behaviour. I will pass you my A reference, so we are working with the same A."

  3. Child to parent: "Fine, I can work with your A reference exactly as I did before, but now you're in charge of A and its lifecycle."

  4. Parent to child: "Works for me, you don't know when I'm done with A anyways."

In this way, responsibility for things declared by with-ref can be lifted up to a parent context, while all children will continue to behave transparently just as they did before. This works across all the facets of your application: component state, domain state, mutable JS objects, or DOM nodes, anything you want to reference or access.

This makes components highly extensible, while at the same time requiring very little consideration about how a component might be extended. with-ref does all the work.

To gain a deeper understanding, let's dig a bit deeper into references, with-ref, and application design.


Next: References and Application Design

Home: Home