How to react to state changes in Reagent vs React?

I am trying to build a block-based richtext editor similar to Notion (with far less features of course) for a project. To get a feel for how it can be done, I adapted this tutorial to ClojureScript with Reagent: Link to GitHub Repo

Since I am still somewhat new to ClojureScript and Reagent (and also never did anything serious with React yet), I appreciate any feedback to improve my adaptation and make it more idiomatic to how Reagent works vs pure React (keep in mind that to keep things simple I do not want to concern myself with re-frame yet).


There is one question I am trying to answer:

In React, you often use a callback function in setState for things you need to do after a state change. As I understand, the callback is triggered after the re-render occurred due to the state change, which is why the author of the original tutorial was able to set the caret position.

Here, I have used reagent/after-render instead, which works, although the behaviour is not quite the same (which is why I had to put it after all state updates instead of just the one in tag-selection-handler as it was with the callback in the pure React code)

I could not find much documentation on reagent/after-render, despite its apparent usefulness. Is there a more idiomatic way to react to state changes in ratoms?


I also made some observations about the use of life-cycle methods in Reagent, maybe someone has alternative suggestions to my approaches here:

I have the impression that, because of ratoms, life-cycle methods have much less use in Reagent. For example, you can initialize internal state in the outer let-binding of form 2/3 components instead, so there is no need for :component-did-mount.

In :component-did-update, the previous state is not available in Reagent, since (please correct me, if I’m wrong) it doesn’t even use this.state.

So I found that sometimes (e.g. for the update to match-sorter in (select-menu) in my editor adaptation) it makes more sense to update state in event handlers where you actually have access to the previous state and use reagent/after-render to react to the state change, so you can check if it actually changed to not trigger an unneccessary re-render.

When passing changes to the parent state (e.g. to update the *blocks ratom of the parent every time the child component re-rendered), it is important to check for differences to prevent an infinite update cycle. I found that with Reagent, it makes more sense to compare differences to the parent state instead of keeping a previous state of the child component around.


Sorry for the long post, I hope it was clear enough.

Methinks you are looking for a ‘place,’ in a Reagent component, to put code that runs when the component state changes, but not on the component’s first render. If I understand your question, then this is what I have to say:

Say I create a simple component:

(defn view-component []
  [:div "Hello."])

This will be rendered once.

Say I want a component to re-render when its props change (“props” is React jargon—they are simply functional arguments in ClojureScript):

(defn view-component [greeting]
  [:div greeting])

This will re-render upon any change to greeting.

Say I want a component that does something special on every re-render, but not on the first render. The best approach I have found—looking for advice here—is like so:

(defn view-component [greeting]
  (r/with-let [!re-render? (atom false)]
    (when @!re-render?
      (println "This is printed only when props change"))
    [:div {:ref (fn [node] (when node (reset! !re-render? true)))}
     greeting]))

This will elide over the println on the first render, because the ref function (React jargon) is not run until after the component renders. When the ref function is run, the !re-render? atom is reset to true, so that on any updated props the (when @!re-render? ...) branch executes.

Reagent experts: does this make sense?, or am I off my gourd?

Thanks for the reply – this is an interesting approach to replace :component-did-update, but not quite what I’m looking for.

My question was about how to react to updates in ratoms similarly to how you would place some code in the callback function of a setState() call in React:

myHandler() {
  ...
  this.setState({ ... }, () => {
    setCaretToEnd(...);
  });
}

Here, setCaretToEnd would cause a side-effect on the DOM state after the re-render (which was triggered by a state change in the event handler), to set the caret in the editor to the last position.

My initial approach was to use after-render:

(fn my-handler []
  ...
  (swap! *state ...)
  (reagent/after-render #(set-caret-to-end ...)))

Which worked, but I am unsure if this is the “right” way to do it or if there is a more idiomatic approach that I am unaware of. Maybe something with Reagents “Reactions” which I have yet to understand.

I use the tengen def-cmptfn macro for any reagent components that require react life cycle integration.

It just makes building these types of components that integrate with JavaScript events much nicer.

Let me know if you use it and have any questions.

That is a very nice macro, thanks for sharing!

As I understand, the biggest advantage is that let-bindings become available downstream through life-cycle stages, letting us write more clean, functional code instead of resorting to mutation and awkward hacks.

This is what I get from looking at the code:

  • :let-mount is evaluated before the first render/mount, hence replacing the outer let-bindings of form-2/3 components in Reagent (or the deprecated :component-will-mount)
  • :let-render is evaluated before every render, replacing any additional let-bindings in the render function
  • :render is just the usual render function, with access to above bindings
  • :post-render is called after render has occurred, combining what you would do in :component-did-mount and :component-did-update, which may be quite useful I believe
  • :unmount is called just before the component unmounts, replacing :component-will-unmount

Additionally, these “magical symbols” become available throughout the life-cycle:

  • this-mounting? to check if the component is in the mounting stage (which is pretty nice)
  • I am not sure what exactly this-cmpt is referring to – is it a this reference to the component class?

I suppose you would declare internal ratoms and functions (for event handlers, etc.) in :let-mount, right?

Reagents form-3 components always feel a bit awkward to use, so I might be using this macro now instead.

That sounds right.

I’ve never had a need for this-cmpt, so not sure about that.

Yes, declare the internal ratoms and event handler functions in the :let-mount. Just keep in mind that this is only called once per component instance, so any component parameters bound in the internal functions will not change.

:post-render is the most useful addition you gain from using this macro, as it is called after each render/each time the props change.

Great! Do you know how to setup clj-kondo for def-cmptfn correctly to not get all these linting errors? I use

:lint-as {taoensso.tengen.reagent/def-cmptfn clj-kondo.lint-as/def-catch-all}

as a workaround, but I thought that maybe there is something more specific? Can’t think of a similar supported macro right now… would have to be a combination of defn and keyword→form pairs.

I’m just excluding it at the moment:

:linters {:unresolved-symbol {:exclude [(taoensso.tengen.reagent/def-cmptfn) (taoensso.tengen.reagent/cmptfn)]}}

Dissatisfied with the situation, I just created a hook for clj-kondo to provide better linting support for the macro - maybe you’ll find it useful: Hook for linting support with clj-kondo · Issue #3 · ptaoussanis/tengen · GitHub

1 Like

I would recommend using React Hooks for this, as it sounds like the behavior you want is already built into React.

e.g.

(defn my-component []
  (let [^js *dirty? (react/useRef false)
        [state set-state] (react/useState "")
        set-caret-to-end ,,,
        my-handler (fn []
                     ;; set the state and mark it as dirty, i.e. we should
                     ;; reset the caret position
                     (set-state "foobar")
                     (set! (.-current *dirty?) true))]
    ;; an effect that will run after every render that `state` changes,
    ;; including the very first render
    (react/useEffect
     (fn []
       ;; we don't want to set caret to the end of every render, only when it's dirty
       ;; e.g. when we set the content of some text input for the user and want to reset
       ;; the caret position
       (when (.-current *dirty?)
         (set-caret-to-end ,,,)
         (set! (.-current *dirty?) false)))
      #js [state])
    ,,,))

;; elsewhere in the app, we render it using the `:f>` keyword to tell reagent to create
;; a function component rather than a class component
[:f> my-component]

This relies on reagent 1.0+ support for React hooks and function components.

2 Likes

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