-
Notifications
You must be signed in to change notification settings - Fork 2
Quick Start
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.
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.
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.
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.
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.
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.
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.
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:
- Resource access
- 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-ref
s in the component
hierarchy goes something like this:
-
Child component
with-ref
: "I declare referencesA
,B
, andC
. This is everything I need to function. I'm in charge ofA
,B
, andC
and will manage lifecycle behaviours, like cleanup." -
Then a parent
with-ref
says to the child: "No, I declare theA
reference, so I can extend some behaviour. I will pass you myA
reference, so we are working with the sameA
." -
Child to parent: "Fine, I can work with your
A
reference exactly as I did before, but now you're in charge ofA
and its lifecycle." -
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