Callbacks as Clojure Records: an approach to generic Reagent components

In my test, I saw two problems. Two functions arn’t equal even without closures:

(=
 (fn [a b] (+ a b))
 (fn [a b] (+ a b)))

:=> false

So I guess this is the first issue.

And the second issue you bring up is that if there are closures, which are really just additional inputs to the function, they should be part of the memoization. Using a name can only solve the first issue, since the second requires value equality.

And the other you bring up, just so I understand, is related to if you call another function which also closes over something? Now that too is indirect input that the memoization isn’t aware off.

I think @TristeFigure code should be recursive already no? If the locals are also of their custom Fn type, when they get compared they will also be compared using the proper equality semantics.

In general you probably want fast reference equality w/ functions, so I would not want to mess with redefining its IEquiv method.

The OP & replies are very interesting in exploring different ways to handle problems of equality in ClojureScript. However, I would again recommend people to use react/useCallback or react/useMemo for these purposes as it’s exactly what they’re made for.

example:

(react/useCallback
  (fn on-change [ev]
    (do-thing-with localA localB ev))
  #js [localA localB])

When used inside the body of a component, this expression will return the callback passed to it. The reference will only change if localA or localB change. This is precisely what you want when rendering components which are memoized (like reagent’s are by default) or other memoized values.

The way they work is by storing the callback & the array of values on the component instance. If you imagine you have a simple component tree like:

            app
           /   \
 componentA     componentB
     |              |
    input         button
                    |
                "click me"

When instantiated, this is represented roughly like (psuedo-code ahead):

{:type app
 :state {,,,}
 :props {,,,}
 :children [{:type componentA
             :state {,,,}
             :props {:on-change #object[Function on-change] ,,,}
             :children [,,,]}
            {:type componentB
             ,,,}]}

Now let’s say that app uses useCallback to memoize the on-change prop passed to componentA:

(defn app [{:keys [localA localB]}]
  (let [on-change (react/useCallback
                    (fn on-change [ev] ,,,)
                    #js [localA localB])]
    ,,,))

What this does is inform React to store the (fn on-change [ev] ,,,) value and the array [localA localB] on first render on the instance, something like:

{:type app
 :state {:hooks [{:type useCallback
                  :fn #object[Function on-change]
                  :deps #js [localA localB]}]}

And on each subsequent render, it will do a comparison between the values of the array in :deps to determine whether it should re-use the one stored on the component instance, or use the freshly created one.

Note that this strategy also allows us to ignore values or even pass in values that aren’t used, but semantically should bust the reference identity, which in practice can be quite helpful when tuning render behavior & performance.

2 Likes

@lilactown i agree with this but to get parity with reagent reactions i believe we’d need to layer in an event-streams or FRP library on top of hooks. Also, i believe render behavior and performance should not need to be tuned, it must be simply correct & optimal always, otherwise we are in the tar pit.

@didibus yes – suppose closure A closes over another closure B; closure B also needs to be stable for A to be stable.

You can easily use reagent reactions with React hooks. A use-reaction hook is a dozen lines of code, maybe.

i believe render behavior and performance should not need to be tuned, it must be simply correct & optimal always, otherwise we are in the tar pit.

This made me laugh out loud :joy: yes, we are all in the tar pit and always will be. There’s no escaping the complexity of trying to describe our desired behavior perfectly to our computers.

Here is a cool scala.js lib with i believe correct and optimal reactivity

This looks a little like Hoplon+Javelin, but with types and a less convenient syntax. I would be curious of your thoughts on that combo. It is basically the same, direct DOM manipulation based on the values of cells, with cell changes propagated directly to the correct place in the Dom.

Could be, I never went deep into hoplon/javelin, it’s really hard to evaluate FRP libs without reading the source … I would be looking for some commentary like this https://github.com/raquo/Airstream (the FRP lib used by Laminar) specifically the section around “frp glitches” and “topological rank”

Ahh javelin is cljs-only

I’ve been working on something similar to airstream (based on Jane Street’s Incremental lib) for Clojure(Script).

But all of that’s very off topic from the OP :slight_smile: reactive programming and VDOM diffing are not at odds with each other. The approach I outlined above (using React’s mechanism to store callbacks on the instance in the component tree) does not prevent you from using some reactive lib like airstream, or reagent, or rxjs, or whatever for managing state external to the component tree.

1 Like

I can’t wait to see it! Clojure needs this

I’m trying to understand how big of a problem this is. Would the constant re-rendering only be a significant problem it the render function does a lot of work? As far as I understand, React would only update the DOM if the actual rendered content changes.

Yes, it’s a problem if the render function does a lot of work. Reagent can be particularly bad in certain cases because its hiccup syntax allocates a bunch of immutable vectors and maps and then parses them into React Elements inside each render function. This is why Reagent wraps all of its components in something similar to the memo higher-order component in order to avoid calling the render function if its props haven’t changed.

It’s also often suggested to move expensive computations outside of the render function, into something external like Reagent reactions or redux, re-frame. Unfortunately, doing this you theoretically miss out a lot on some potential benefits of React such as time slicing and concurrent mode. These aren’t enabled in the current production version of React right now, as they’re still experimental, so you’re not currently missing out on much.

To take advantage of time slicing & concurrent mode in the future, you actually want to do more work in your render function; because of this, you need more fine-grained control over when certain work is done in order to not naively do a bunch of expensive calculations every render.

This is why constructs like useMemo and useCallback exist. Just memoizing a whole component based on props is often not enough if you want to do lots of work inside of a components render.

1 Like

Yeah me too. @dustingetz highlighted some problems. In practice, for me, where most is just forms rendering, even if there are re-renders, it’s almost always snappy for me.

So for me the problem, so far, is theoretical in when it really is visible, but of course, there are use-cases where it is a problem in practice too.

Therefore this is a very interesting topic imo :slight_smile:

I am kind of swamped with work at the moment, but it would be interesting to do the same tests in javelin and do a proper write-up. Maybe I will when I get the time. :slight_smile: Reading the Airstream section on glitches and without testing anything, I think Javelin handles it similarly.

1 Like

Vanilla React without any optimization at all is fast enough for a wide class of CRUD apps, but if you need to scan results in rendering e.g. to validate specs on database resultsets, or to figure out what the column span is of a sparse resultset, that can make your form fields get visibly laggy as you type if that computation runs carelessly.

PS we have not yet discussed “deref thrashing”, which is when properly stabilized Reagent reactions recompute non-optimally (Reagent makes no attempt to intelligently plan the order in which queued updates should run and therefore often runs them in the wrong order, triggering needless cascades of partial recomputes). This is where a little category theory goes a long way - Applicative typeclass in particular is useful to indicate to the reaction interpreter which updates should run in parallel.

Hoplon/Javelin source is worth a read. Small and wonderfully bare.

Never built anything large with it, but enjoyed the ergonomics for quick prototypes. Remember some confusion over how to form the signal graph over collections (eg vector-of-cells vs cell-of-vector), but that’s far less to learn than the proliferation of abstractions in most UI libs.

2 Likes

If i had this situation (rare because re-frame doesn’t encourage much prop drilling) and it was performance sensitive, I’d reify the callbacks so they tested = to before. This technique requires that you use a form-2 view.

Code sketch:

Have a utility function like this around the place:

(defn cb-factory-factory
  [real-callback]
  (let [*args1     (atom nil)
        cb-wrapper (fn [& args2]
                     (apply real-callback (concat @*args1 args2)))]
    (fn [& args1]                  ;; id comes in as args 
      (when (not= args1 @*args1) 
        (reset! *args1 args1))
      cb-wrapper)))               ;; <-- always return the same fucntion

Then use it:

(defn some-view 
  [_]
  (let [on-click          (fn [id] (re-frame/dispatch [:something id]))
        on-click-factory  (cb-factory-factory on-click)
        on-change         (fn [id event] (re-frame/dispatch [:it-changed id (-> event .-target .-value)]))
        on-change-factory (cb-factory-factory on-change)]

    (fn [some-id]
      [:<>
       [:div  {:on-click (on-click-factory some-id)}]        
       [:input {:type      "text" 
                :value     "Hello"
                :on-change (on-change-factory some-id)}] ])))

Sounds like I should document this??

BTW, do not take this as the only way to do it. There’s a bunch of simple variations on this kind of approach subject to your needs, particularly when, in re-frame, most of your handlers are dispatching allowing you to be simpler and more specific.

1 Like

I’m both late to this topic and could be missing something, but if I squint the original proposal re: records looks like the Delegate Pattern that gets used a ton in NextSTEP / Cocoa (the ObjC Mac OS framework).

As patterns go, it did a good job of standing the test of time, in that Mac UI components were nicely generic and re-usable. The newer Swift Mac OS/iOS zeitgeist I’m seeing is more functional and drifting towards callbacks closed over their dependencies.

1 Like

It has taken me a while to get back to this, but I’ve now written a tutorial page describing my solution:
http://day8.github.io/re-frame/on-stable-dom-handlers/

2 Likes

Another great page of the re-frame docs. Thanks for sharing this!

1 Like

This topic was automatically closed 182 days after the last reply. New replies are no longer allowed.