-
Notifications
You must be signed in to change notification settings - Fork 2
Advanced Usage
with-ref
cleanup events are dispatched asynchronously and handled in
the next event processing frame after the with-ref
's associated
component unmounts.
Only transient references, ones that were created by a with-ref
,
are ever cleaned up. with-ref
will warn you if you attempt to write
to transient state that has already been cleaned up. Writing to
transient state is not in and of itself an error, but often it is a
sign that the application is doing something unexpected.
Cleanup is implemented using a single multimethod
reflet.core/cleanup
that dispatches on the unique attribute of the
reference in question. There is a default method defined that takes
any with-ref
reference and removes any associated entity from the
multi-model db.
Special implementations have been extended for the following unique attributes:
-
:js/uuid
: Removes the associated JS object from the Reflet mutable state registry, calling any:destroy
method that may have been declared at the time of registry -
:el/uuid
: Removes the DOM element from the Reflet mutable state registry, calling any:unmount
method that may have been declared at the time of registry
Unless you are doing something quite advanced, you will probably not
need to extend your own cleanup methods. However, if you do, just
follow the patterns already in reflet.core
. For example here is the
:debug/id
implementation:
(defmethod cleanup :debug/id
;; Cleanup behaviour is specific to the debugger.
[{db :db :as cofx} [_ ref :as event]]
(let [handler (get-method cleanup :default)
default-fx (handler cofx event)]
(->> {:log [:debug "Debug cleanup" ref]
:db (untap (:db default-fx db) ref)}
(merge default-fx))))
Note that the signature for the multimethod is the same as a Re-frame
event handler, where all of the usual cofx are provided by the first
argument. It also returns regular Re-frame fx, extending the
:default
implementation's fx by merging them with the new ones.
There is one important rule when extending cleanup methods: never
re-dispatch new events. This can cause race conditions by pushing
the re-dispatched event after the :component-did-mount
methods of
the next component cycle. This has nothing to do with the Reflet per
se, just a general consideration when working with React lifecycles.
Each unique id attribute has an associated unique value type. The
default type is cljs.core/UUID
. This means that when you make a new
reference, say :cmp/uuid
:
(db/random-ref :cmp/uuid)
=>
[:cmp/uuid #uuid "bf8cc02d-271d-4779-843c-e3829f800cb6"]
The unique value in the reference will be a UUID.
If you want something other than this default, you need to define your
own reference generator. This can be done by extending the
reflet.db/random-ref-impl
multimethod.
For example, say you need unique keyword values for the unique
attribute :my.keyword/id
:
(require '[reflet.db :as db])
(defmethod db/random-ref-impl :my.keyword/id
;; Must return a reference.
[attr]
[:my.keyword/id (keyword "id" (str (random-uuid)))])
Now you can generate them anywhere in your app:
(db/random-ref :my.keyword/id)
=>
[:my.keyword/id :id/3e57d7a2-9148-47b0-81b8-950ed11f74d0]
(f/with-ref {:my.keyword/id [self]}
self)
=>
[:my.keyword/id :id/b6dc1eb8-070c-4bab-9364-13ec8a44568a]
However there is a caveat! While any value type can participate as a
persistent reference throughout reflet, only JS Object values can
participate as transient references generated by with-ref
. JS
literal values cannot. So UUIDs and keywords are ok in transient
references, but string literals are not:
(defmethod db/random-ref-impl :str/id
[_]
[:str/id (str (random-uuid))])
;; If done in a reactive context, this will attempt to produce a
;; transient reference, and hit an error
(f/with-ref {:my.keyword/id [self]}
self)
=>
Execution error (Error) at (<cljs repl>:1).
No protocol method IWithMeta.-with-meta defined for type string: d9959a4c-29af-4970-b04d-6405670d9fcb
Superficially this is because Reflet implements transient references
using metadata on the unique values, and only Object values can
implement cljs.core/IWithMeta
and cljs.core/IMeta
. But the deeper
reasons is that JS literals do not have lifecycles like objects, where
there can be any sense of cleanup as it relates to transient
references.
Getting back to cljs.core/IWithMeta
and cljs.core/IMeta
, they have
already been implemented for cljs.core/UUID
s and
cljs.core/Keyword
s. However, if you need some other value type, you
have to implement them yourself.
For example, each of the JS literal types has a corresponding Object type, the most relevant being numbers and strings:
-
"unique"
->(js/String. "unique")
-
1
->(js/Number. 1)
And each such Object type could be extended to particpate in
references where literals cannot (you would also likely want to
implement cljs.core/IEquiv
to be maximally useful).
But remember, all this is only relevant for transient references
created by with-ref
. There are no such complications for persistent
references. And even for the vast majority of transient with-ref
use cases, either UUIDs or keywords should be sufficient.
Warning: The same with-let is being used more than once in the same reactive context.
The semantics on how to avoid this warning are explained in the References and Application Design document. They are simple enough to follow, and so you don't really need to go beyond that. But if you really want to turn over the rock, it comes down to three things:
-
with-ref
is implemented usingwith-let
under the hood -
Caching of
with-let
values is done via cached Reagent reactions, and each component reactive context gets its own Reagent reaction cache (this is not the same as the Re-frame subscription cache). -
The identity of these
with-let
reactions is resolved by agensym
in the lexical scope of thewith-let
during macro expansion, not at runtime
To understand the implications, consider a component that has a
single with-ref
:
(defn child-maybe-component
[props]
(f/with-ref {:cmp/uuid [self]}
[:div (str self)]))
Here, the identity of the cached Reagent reaction is determined when the macro is expanded during Clojurescript compilation, well before the function is ever executed.
Then, let's say you use function invocation ()
on a component
twice in some parent context:
(defn parent-component
[props]
[:div
(child-maybe-component props)
(child-maybe-component props)])
Despite two invocations of with-ref
, the cached Reagent reactions
under-the-hood will both resolve to the same id that was determined
lexically during macro expansion. But if their cached reactions are
the same, then the references produced by both with-ref
s will also
be the same. Unfortunately that is almost never the intent behind such
code, and the reason for the with-let
warning.
However, when you use component invocation []
you introduce a new
reactive context around the component. Each component reactive context
comes with its own reaction cache, so it doesn't matter that the
lexically determined reaction ids are the same, they will hit
different caches:
(defn parent-parent
[props]
[:div
[child-maybe-component props]
[child-maybe-component props]])
Each with-ref
now has its own reaction cache, and will generate
different references.
Next: Example Client
Home: Home