-
Notifications
You must be signed in to change notification settings - Fork 2
References and Application Design
At the core of Reflet is the with-ref
macro. The with-ref
macro
creates references that can be used as unique identifiers anywhere in
your application. References, also referred to as "idents" in Datomic
and EQL, are two element tuples containing a unique attribute and a
unique value:
[:cmp/uuid #uuid "bf8cc02d-271d-4779-843c-e3829f800cb6"]
[:user/email "my@email.com"]
with-ref
can generate references with any unique attribute out of
the box. For example, if you want to generate unique references for
managing focus across components, no special configuration is
required:
(f/with-ref {:focus/id [id]}
id)
;; =>
[:focus/id #uuid "1a9785dc-1ef2-4eed-8342-01fe76367bd1])
However, if you want a reference to participate in graph data, then the Reflet db needs to be configured to work with the unique attribute in the reference. The Reflet db is schema-less, so the set of unique attributes is defined via a configuration map when you first initialize the Reflet db. See the Configuration document for more details.
Once a unique attribute has been configured for graph data, these references implicitly assert the existence of one and only one associated entity that also contains that same attribute-value pair:
[:cmp/uuid #uuid "bf8cc02d-271d-4779-843c-e3829f800cb6"]
;; <=>
{:cmp/uuid #uuid "bf8cc02d-271d-4779-843c-e3829f800cb6"
:kr/name "Name"
:kr/description "Other data"}
Even if the entity has not actually been instantiated yet, the
association is implicit. The semantics of uniqueness mean that there
is no other entity in the program that can have the value #uuid "bf8cc02d-271d-4779-843c-e3829f800cb6"
for the attribute
:cmp/uuid
. In this way, the tuple [:cmp/uuid #uuid "bf8cc02d-271d-4779-843c-e3829f800cb6]
can be used to uniquely refer
to that entity anywhere in the application. This is important: it's
the attribute-value pair, not the unique value alone, that makes a
reference.
With an understanding of references, we can turn our attention back to
with-ref
.
Consider the
reflet.client.ui/player
audio component in the Reflet example client:
(require '[reflet.core :as f])
(defn player
[props]
(f/with-ref {:cmp/uuid [player/self]
:js/uuid [player/context player/source]
:el/uuid [player/el]
:in props}
[:div ...]))
This with-ref
asserts four different references :in
the component
props
map. The keys in the with-ref
map declare the unique
attributes that participate in the references, and the values specify
where in the props
map those references will be bound. For each
symbol or keyword in a binding vector, a reference will be added to
props
at that attribute. The semantics are almost the inverse of
Clojure map destructuring. Also, each unique attribute is associated
with an extensible reference value
type,
the default type being cljs.core/UUID
.
In this example, the with-ref
asserts that the props
map should
have at least the following attributes:
{:player/self [:cmp/uuid #uuid "bf8cc02d-271d-4779-843c-e3829f800cb6"]
:player/context [:js/uuid #uuid "3e57d7a2-9148-47b0-81b8-950ed11f74d0"]
:player/source [:js/uuid #uuid "1a9785dc-1ef2-4eed-8342-01fe76367bd1"]
:player/el [:el/uuid #uuid "8f4549c8-20de-4c37-a4ec-6072b40e512f"]}
These references will be merged with any other values that are passed
in as props. If the props map already contains values at a binding
attribute, say :player/el
, then with-ref
will not generate a new
reference or overwrite the existing value:
{:player/self [:cmp/uuid #uuid "bf8cc02d-271d-4779-843c-e3829f800cb6"]
:player/context [:js/uuid #uuid "3e57d7a2-9148-47b0-81b8-950ed11f74d0"]
:player/source [:js/uuid #uuid "1a9785dc-1ef2-4eed-8342-01fe76367bd1"]
:player/el [:other/id :i-was-passed-in-with-props]
:so/was-i :but-not-a-reference}
This is especially important in a React component context.
When used in a React component with-ref
makes a distinction between
two types of references:
-
Newly created references that
with-ref
adds to props:These are considered local in scope to the
with-ref
reactive context, and will be cleaned up once its related component is unmounted. These references will also be marked as:transient true
via metadata. -
External references that are passed in with props:
These are considered external in scope to the
with-ref
reactive context, and will be left alone during component unmount.
Or summed up in a single statement:
Each
with-ref
is responsible only for the new transient references that it creates, and for cleaning them up when its associated component unmounts.
with-ref
can also be used outside of a reactive context, for example
in tests or in the REPL, but the refs will not be transient, and there
will be no cleanup.
Cleanup behaviour is specific to the unique attribute that
participates in the reference. The default built-in behaviour is to
remove any associated entity from the db. There are also special
behaviours predefined for :js/uuid
and :el/uuid
references, which
is discussed in more detail in the Mutable State document. See the
Advanced Usage document for how to extend your own cleanup methods
for other attributes.
References that were not created by any with-ref
are never
cleaned up and considered persistent. For example, any references
that you might receive from a remote data source will be persistent.
You can force a with-ref
to create persistent refs using the
:persist
option:
(f/with-ref {:cmp/uuid [player/self]
:persist true
:in props}
...)
Even if this with-ref
creates the :player/self
ref, it will never
be cleaned up. On the other hand, :persist true
has no affect on
already existing references that are passed in to the with-ref
. If
an external reference is transient, this with-ref
cannot turn it
persistent.
There is also a :meta
option that can be used to attach arbitrary
metadata to your newly created references. This can later be retrieved
with reflet.db/ref-meta
:
(f/with-ref {:cmp/uuid [player/self]
:meta {:provisional true}
:in props}
(db/ref-meta self))
=>
{:provisional true, :transient true}
;; The :transient true would only be here in a reactive context.
Again, any external references that are passed into this with-ref
cannot have their metadata changed.
Remember: each with-ref
is only responsible for the new
references that it creates.
In addition to adding references to props, with-ref
will expose
local bindings for each of the declared references, whether they were
passed in or not.
So the following:
(f/with-ref {:cmp/uuid [::self]
:js/uuid [:player/context player/source]
:el/uuid [player/el]
:in props}
[self context source el])
Will return:
[[:cmp/uuid #uuid "bf8cc02d-271d-4779-843c-e3829f800cb6"]
[:js/uuid #uuid "3e57d7a2-9148-47b0-81b8-950ed11f74d0"]
[:js/uuid #uuid "1a9785dc-1ef2-4eed-8342-01fe76367bd1"]
[:other/id :i-was-passed-in-with-props]]
and corresponds to the values in the props
map.
Alternatively, instead of adding references into an existing props
map, you can generate an entirely new map if you choose a props symbol
that is not already locally bound:
(defn player
[props]
(f/with-ref {:cmp/uuid [player/self]
:js/uuid [player/context player/source]
:el/uuid [player/el]
:in new-props}
[child-component new-props]))
Above, new-props
will be an entirely new map containing only the new
references generated by the with-ref
.
You can also omit the :in props
binding completely, using with-ref
to generate only locally bound references:
(f/with-ref {:cmp/uuid [a ::b]}
;; `a` and `b` are only available as local bindings
[child-component a b])
Also note it is perfectly fine to have nested with-ref
s:
(defn player
[props]
(f/with-ref {:cmp/uuid [player/self]
:in props}
(f/with-ref {:cmp/uuid [player/local]}
[:div {:on-click #(f/disp [::event local])}
[child-component props]])))
Finally, there is an alternative syntax that allows more flexible binding of local symbols to props. Once again this corresponds closely to the semantics of Clojure map destructuring:
(f/with-ref {:cmp/uuid {self :player/self}
:js/uuid {ctx :player/context
src :player/source}
:el/uuid {dom :player/el}
:in props}
[self ctx src dom props])
;; =>
[[:cmp/uuid #uuid "bf8cc02d-271d-4779-843c-e3829f800cb6"]
[:js/uuid #uuid "3e57d7a2-9148-47b0-81b8-950ed11f74d0"]
[:js/uuid #uuid "1a9785dc-1ef2-4eed-8342-01fe76367bd1"]
[:el/uuid #uuid "8f4549c8-20de-4c37-a4ec-6072b40e512f"]
{:player/self [:cmp/uuid #uuid "bf8cc02d-271d-4779-843c-e3829f800cb6"]
:player/context [:js/uuid #uuid "3e57d7a2-9148-47b0-81b8-950ed11f74d0"]
:player/source [:js/uuid #uuid "1a9785dc-1ef2-4eed-8342-01fe76367bd1"]
:player/el [:el/uuid #uuid "8f4549c8-20de-4c37-a4ec-6072b40e512f"]}]
With this simple way to create references, you can then use them anywhere you need an id, name or reference to something. You can reference domain data, mutable JS objects, it really doesn't matter.
For example, you can pass them to event handlers to reference component local app state:
(require '[reflet.db :as db])
(defn play-button
[{:keys [player/self]}]
[:button {:class "primary"
:on-click #(f/disp [::play self])}
"Play"])
(f/reg-event-db ::play
;; This handler mixes graph and non-graph operations
(fn [db [_ self]]
(-> db
(db/assoc-inn [self :player/state] ::playing)
(assoc ::flag true))))
(defn player
[props]
(f/with-ref {:cmp/uuid [player/self]
:in props}
[:div
[play-button props]
...]))
Above, the :player/self
ref is used to store the current state of
the audio player as a graph entity in the Re-frame db. db/assoc-inn
is one of several graph mutation functions that can be used in event
handlers. The multi-model db, and the full set of db mutation
functions are discussed in detail here.
With this event handler defined, you can then subscribe to reactive changes in the graph data:
(f/reg-pull ::player-state
(fn [self]
[[:player/state
{:player/track [:kr.track/name
:kr.track/uri]}]
self]))
(defn player
[props]
(f/with-ref {:cmp/uuid [player/self]
:in props}
(let [{:player/keys [state track]} @(f/sub [::player-state self])]
[:div
[play-button props]
[:div (:kr.track/name track)]])))
f/reg-pull
and the graph query API are discussed in more detail
here, but for now the most important thing to note
is that the :player/self
ref is the only thing that is passed around
to complete the reactive loop. Thus references become the parameters
that connect your application together. Combined with the semantics of
with-ref
behaviour, this encourages pluggable components and
excellent APIs.
To see this more clearly, consider how a parent component might extend
the functionality of player
:
(defn extended-player
[props]
(f/with-ref {:cmp/uuid [player/self] ; Parent also asserts :player/self
:in props}
(let [state @(f/sub [::extended-query self])]
[:div
[player props] ; Passes props to child
[:div {:on-click #(f/disp [::extended-event self])}
"Extended Function"]
...])))
Here, the parent component also asserts the :player/self
ref in
props
, and then passes those props
along to the child
component. But now, the parent with-ref
is in control of the ref
lifecycle, because that's where the ref was created. The child works
exactly like it did before, but because the ref is passed in, when
player
unmounts it does not cleanup :player/self
. This way the
parent extended-player
retains access to any referenced resource.
In essence, responsibility for the thing being referenced by
:player/self
is "lifted" up to the parent context, where the parent
extends the functionality of player
. This pattern can be repeated
over and over higher up the component hierarchy, even though we really
didn't design extended-player
or player
with this in
mind. with-ref
does all the work, and the child components will
continue to function transparently.
Does this mean that every ref declared in a with-ref
becomes part
of the component's public API? Well, it's up to you. You can isolate
some or all of the references by writing a with-ref
with either a
different :in props
binding symbol, or no props binding symbol
at all:
(defn player
[props]
(f/with-ref {:cmp/uuid [player/self]
:el/uuid [player/el]
:in props} ; Public props, part of component API
(f/with-ref {:js/uuid [player/context player/source]
:in props-private} ; Private with different props binding symbol
...
[child-component (merge props props-private)])))
Since no references are ever passed into the inner with-ref
, no
parent context can "lift" control of the refs in private-props
.
Ultimately there are two big benefits that this seemingly simple macro brings:
- Resource access
- Transparent lifecycle management
And because they are just unique ids, they can be used almost anywhere, not just for graph data and queries.
This exact principle that works with references to app state, can be
applied to references to mutable JS objects or DOM elements.
In fact, most aspects of your component API will benefit
from this kind of with-ref
parameterization.
Reinforcing just how helpful with-ref
can be for API design, is the
powerful debugging paradigm that emerges almost for free with the use
of with-ref
. See the Debugging document for more info.
When it comes to data attributes Reflet is agnostic about how you model your data. However, one recommendation is to use distinct sets of attributes between your component local state and domain data.
Reflet provides a default set of unique attributes so that all the
design patterns that use with-ref
work out of the box:
-
:system/uuid
for domain data -
:cmp/uuid
for component local state -
:js/uuid
for JS Object references -
:el/uuid
for DOM element references
But you might end up defining your own, almost certainly for your domain data. See the Configuration document for how to define your own unique attributes.
Just keep in mind that the debugger was implemented using the default set. Therefore when loaded, the debugger will always add those attributes to any set that you define in order to support its implementation.
Another point about attributes: most of the documentation uses keywords for attributes. Of course if you're integrating with anything other than Clojure, your attributes are likely going to be strings. Reflet makes no assumptions in this regards, you can use either.
(f/reg-pull ::player-state
(fn [self]
[[:player/state
{"player.track" ["kr.track.name"
"kr.track.uri"]}
self]]))
with-ref
leverages the Reagent reagent.core/with-let
macro under
the hood, and so many of the same usage semantics also apply. This
section explains the meaning behind the following warning, and how to
avoid it:
Warning: The same with-let is being used more than once in the same reactive context.
This warning only happens under a couple of specific scenarios, so you
could skip this section and come back to it later if you encounter
it. Just know that while this is a warning, it is not harmless! Its
presence indicates that one or more with-ref
s or with-let
s have
likely produced identical references or values even thought that was
not the intent.
Anyways, let's break this down:
-
The same with-let
: eachwith-let
is uniquely identified by its lexical scope in code at compile time. Literally which line of code. This is an implementation detail of Reagent'swith-let
. -
reactive context
: the React component in which thewith-let
is invoked at runtime. Here, the the distinction between function invocation()
and component invocation[]
is very important. -
used more than once
: invoked more than once per render phase
The golden rule to avoid this warning could be re-stated as such:
Given that
with-ref
is uniquely identified by its lexical scope in code at compile time, each uniquewith-ref
should be invoked only once per render of its surrounding React component.
The semantics just described apply equally to with-ref
and
with-let
: you'll get the same warnings, and usage patterns.
Bad:
1 (defn component
2 [props xs]
3 [:div
4 (doall
5 (for [[i x] (map-indexed vector xs)]
6 (f/with-ref {:el/uuid [el]}
7 ...)))])
Because of the for
loop, the with-ref
on line 6
of component
is invoked multiple times per render of component
. This will
generate a warning.
In this case the warning could be resolved by wrapping the with-ref
in a sub-component:
1 (defn child-component
2 [props]
3 (f/with-ref {:el/uuid [el]}
4 ...))
5
6 (defn component
7 [props xs]
8 [:div
9 (doall
10 (for [[i x] (map-indexed vector xs)]
11 ^{:key i} [child-component x]))])
Now the with-ref
on line 3
of child-component
is invoked once
per render of child-component
.
Also bad:
1 (defn child-maybe-component
2 [props]
3 (f/with-ref {:el/uuid [el]}
4 ...))
5
6 (defn parent-component
7 [props]
8 [:div
9 (child-maybe-component props)
10 (child-maybe-component props)])
Here, using function invocation ()
on child-maybe-component
means
that the with-ref
on line 3
of child-maybe-component
is actually
invoked twice within its component context: parent-component
. This
could be resolved by simply using component invocation on the
child-maybe-component
s.
6 (defn parent-component
7 [props]
8 [:div
9 [child-maybe-component props]
10 [child-maybe-component props]])
Next: Multi Model DB
Home: Home