Skip to content

Advanced Usage

zalky edited this page Feb 7, 2023 · 2 revisions

with-ref Cleanup

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.

Unique Attribute Values

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/UUIDs and cljs.core/Keywords. 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.

same with-let Warning: Digging a Bit Deeper

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:

  1. with-ref is implemented using with-let under the hood

  2. 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).

  3. The identity of these with-let reactions is resolved by a gensym in the lexical scope of the with-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-refs 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