Skip to content

General Development Tips

zalky edited this page Feb 7, 2023 · 2 revisions

This document contains a grab-bag of tips, gotchas and information that is generally relevant for frontend development. They are not all necessarily specific to development with Reflet.

Reactions Outside the Render

Avoid dereferencing subscriptions, reactions, or ratoms outside the render phase. They can be out of sync with React props, which can introduce subtle concurrency problems if you're not careful. This includes all the component lifecycles besides :reagent-render, as well as any callbacks used in React :refs.

Component Lifecycles

Take care with the interactions between :component-did-mount and :component-will-unmount across component lifecycles. There is no way for mount methods to safely see the state effects of unmount methods from a previous lifecycle. You can introduce concurrency problems if the :component-will-unmount and the :component-did-mount produce mutations on the same piece of app state.

The reason why is fairly complicated, and requires a full itinerary of what happens during the React lifecycle of a single component:

  1. Reagent constructs the component with an initial props value. Closures are created around the lifecycle methods.

  2. React runs an initial render using the initial props value.

  3. React computes which components are no longer needed, and their reaction on-dispose functions and :component-will-unmount methods are called, in that order. Note that the reaction on-dispose includes with-let finally clauses and with-ref cleanup methods.

  4. The callback function passed to React :refs fires for a newly mounted component. However, this function will have closed over the initial props value, and will not see any changes from the unmount methods in step 3.

  5. Next, the :component-did-mount is called, but again with the initial React props. Any effects from the :component-will-unmount or on-dispose methods that had just been called in step 3 will not be seen by this method.

  6. Finally the component has a chance to re-render in response to changes from unmount methods, and receive a new set of props. But by this point the :component-did-mount lifecycle is long gone, and may have produced side-effects that conflict with those of the unmount methods.

Transient Refs Gotchas

Careful when passing transient refs around outside the component tree in which they were made. In a very real way they are tied to things that are essentially ephemeral. You can end up writing to state after that state has already been cleaned up. Reflet will warn you about this, but it is best avoided. Three good rules of thumb are:

  1. Avoid transient refs as joins in persistent data entities

    ;; Assuming that [:cmp/uuid #uuid "b"] is transient,
    ;; but [:system/uuid #uuid "a"] is persistent.
    {::db/data
     {[:system/uuid #uuid "a"] {:system/uuid #uuid "a"
                                :kr/join     [:cmp/uuid #uuid "b"]} ; <- Careful!
    
      [:cmp/uuid #uuid "b"]    {:cmp/uuid #uuid "b"
                                :cmp/flag true}}}
  2. Avoid transient refs as joins in link entries

    ;; Assuming that [:cmp/uuid #uuid "b"] is transient.
    {::db/data
     {:my.link/entry        #{[:cmp/uuid #uuid "b"]} ; <- Careful!
    
      [:cmp/uuid #uuid "b"] {:cmp/uuid #uuid "b"
                             :cmp/flag true}}}
  3. Do not store transient refs using non-graph operations, where they may persist beyond the with-ref lifecycle

In each of these three cases, after the entity referenced by [:cmp/uuid #uuid "b"] is cleaned up, the other references will remain (assuming the user does not do additional cleanup). While this is relatively harmless, these left-over transient references could then find their way into an entity transaction, and lead to ephemeral state being stored again after it has been cleaned up.

References vs Unique Values (UUIDs)

Careful with where you use references and where you use unique values (like UUIDs):

;; reference
[:cmp/uuid #uuid "bf8cc02d-271d-4779-843c-e3829f800cb6"]

;; unique value
#uuid "bf8cc02d-271d-4779-843c-e3829f800cb6"

In general you should prefer refs over unique values everywhere. But especially in:

  1. Prop attributes in components
  2. Join attributes in db entities
;; Correct
[my-component {:my/prop [:cmp/uuid #uuid "bf8cc02d-271d-4779-843c-e3829f800cb6"]}]
;; Correct
{:kr.track/artist [:system/uuid #uuid"3e57d7a2-9148-47b0-81b8-950ed11f74d0"] ...}

;; Wrong
[my-component {:my/prop #uuid "bf8cc02d-271d-4779-843c-e3829f800cb6"}]
;; Wrong
{:kr.track/artist #uuid"3e57d7a2-9148-47b0-81b8-950ed11f74d0" ...}

The only place you should prefer unique values is:

  1. Unique attributes values in db entities:
;; Correct
{:cmp/uuid #uuid "bf8cc02d-271d-4779-843c-e3829f800cb6" ...}
;; Wrong
{:cmp/uuid [:cmp/uuid #uuid "bf8cc02d-271d-4779-843c-e3829f800cb6"] ...}

Props Attributes vs Unique Attributes

Careful not to confuse props attributes with unique attributes, especially when merging.

In components:

;; prop attribute --v          v--- unique attributes
[my-component {:my/prop [:cmp/uuid #uuid "bf8cc02d-271d-4779-843c-e3829f800cb6"]}]

In db entities:

;; v-- unique attribute  v---- prop attribute
{:cmp/uuid (second (:my/prop props))}

Home: Home