off-topic

jyn 2025-11-02T14:53:57.632369Z

i wrote a blog post https://jyn.dev/build-system-tradeoffs/

๐Ÿ˜ 1
๐ŸŽ‰ 2
Stephan Renatus 2025-11-04T08:25:00.930569Z

learnt something from it. thanks for sharing!

โค๏ธ 1
borkdude 2025-11-02T20:25:43.063389Z

In React (or similar) what are your most used lifecycle methods: component did mount / did unmount. Others? And why are you using them? I'm thinking about adding :ref to reagami (have it working locally) but wonder if you need anything beyond that, ever... ๐Ÿงต

borkdude 2025-11-02T20:25:50.252899Z

Respond in thread

p-himik 2025-11-02T20:34:24.362329Z

โ€ข :component-did-mount - setting up the state of complex components that requires for the DOM node to be present. Often needed when working with vanilla JS components. I suspect that most of the usages could be replaced with a :ref function or useEffect. โ€ข :component-will-unmount - cleanup. E.g. removing a global event listener. Could be replaced with useEffect with a function that returns a cleanup function. โ€ข :component-did-update - similar to the did-mount function, but for syncing updates to the state with whatever non-React thing is there I'm not completely certain about usages of useEffect - it's not 100% compatible with did-mount/will-unmount because it will run its setup/cleanup functions multiple times.

borkdude 2025-11-02T20:35:54.029589Z

would it be a problem for you if you only had did-unmount instead of will-unmount?

p-himik 2025-11-02T20:39:22.023649Z

FWIW, Reagent/React don't have did-unmount at all. :) It might cause problems, yes. E.g. suppose I use some vanilla JS component that's passed a DOM node and stores it internally. In will-unmount I then call something like (.cleanup vanilla-component) - and I have no idea whether it tries to access the original node or not. And some vanilla JS libs do set their state via properties on DOM nodes.

borkdude 2025-11-02T20:40:06.665529Z

yes, the original node will be passed but it won't be connected to the DOM anymore

borkdude 2025-11-02T20:42:02.628149Z

with :ref you can already do this, just store the element somewhere and when the ref fn is called with nil, you can just do something with the element

p-himik 2025-11-02T20:42:35.657359Z

I really don't know whether it would cause problems for the code that I have, but I do use that (.cleanup vanilla-component) pattern where the vanilla component can do absolutely anything during its cleanup. It could be e.g. that the vanilla component actually requires multiple components and a cleanup requires first cleaning up the children and then the parent, and the children cleanup traverses the DOM tree. Convoluted, but it's JS so who knows what lurks below.

borkdude 2025-11-02T21:09:38.737169Z

oh yeah you need did update whenever a JS component must change according to some argument given to a component

2025-11-02T21:09:42.436389Z

You're probably aware, but class components haven't been a thing that's recommended in at least half a decade now in JS React, so I'm wondering if keeping to an old paradigm in Clojure makes sense since it would be increasingly foreign to people, and would lack corresponding docs in JS-land.

borkdude 2025-11-02T21:10:36.070659Z

I'm not even thinking about components yet, nor classes. Just hiccup -> dom nodes

borkdude 2025-11-02T21:10:45.790179Z

(similar to replicant I guess)

2025-11-02T21:11:25.910509Z

Ah! My bad then. I saw component-did-mount/etc lifecycle hooks that were used in class-based react components some 7 or so years ago, and figured they are connected.

borkdude 2025-11-02T21:12:25.501359Z

yeah similar. I guess you would do it differently in React now? say you have a d3 graph thing going on. And it depends on x and y arguments to a function that returns hiccup. How would you update it in React nowadays?

2025-11-02T21:13:17.810739Z

React retired class-based components a long time ago (actually around the same time I got in Clojure, so ~7 years ago), in favour of functional hook based system instead (i.e useEffect).

2025-11-02T21:14:18.422949Z

So if I want to do something reactive if say name changes, I'd do:

useEffect(() => console.log('changed'), [name]);
With the first argument being a callback to be called, and second being an array of dependencies to listen to.

2025-11-02T21:16:26.942159Z

There's other hooks, too, of course, like useRef . You can utilize useRef and useEffect together to essentially do a on-mount if you wanted, but there's many different combinations of things you can do, and you can of course create your own hooks.

2025-11-02T21:23:00.706589Z

So a reply to your original question would be to perhaps call the hiccup(x, y) function in a useEffect if X / Y state changed.

function MyComponent() {
  const [x, setX] = useState(10);
  const [y, setY] = useState(20);

  return (
    <Hiccup x={x} y={y} />
  );
}
Here because of reactivity the chart will re-render if the x or y state changes, with the Hiccup here being the function / component that returns Hiccup for the x and y props. Alternatively you can do something like:
function MyComponent() {
  const [x, setX] = useState(10);
  const [y, setY] = useState(20);
  const [hiccup, setHiccup] = useState([]);

  useEffect(() => {
    setHiccup(createHiccup(x, y));
  }, [x, y]);

  return (
    <>{hiccup}</>
  );
}
So now whenever X or Y change, it will trigger a useEffect that is listening to those changes, call the createHiccup function and set the returned value of it to the hiccup state, which then causes the component itself to re-render the new hiccup. This would need something external to change the x and/or y though, like a button click or a parent component sending in new data, but you get the idea.

2025-11-02T21:25:31.941349Z

here the useState , useEffect being built-in React hooks, not custom made code.

2025-11-02T21:30:30.622069Z

A Clojure equivalent could look something like:

(defn my-component []
  (let [[x, setX] (useState 10)
        [y] (useState 20)
        [hiccup, setHiccup] (useState nil)]
    (useEffect
      (fn []
        (-> (creatHiccup x y)
            setHiccup))
      [x, y])
    [:div
      [:button {:on-click #(setX (inc x))}
        "change X state, re-render hiccup"]
      hiccup]))

borkdude 2025-11-02T21:32:42.296529Z

I don't mean mixing hiccup and react. My "react replacement" is just a thing which renders hiccup to DOM nodes directly. My question is: how does one update a D3 component that is mounted to a real dom node and has to update on every re-render (given that arguments x and y change to a component / function) 10 years ago this was done using didComponentUpdate. How does one do that now?

2025-11-02T21:33:44.933529Z

So you mean that D3 is not rendered with React at all?

borkdude 2025-11-02T21:34:05.969149Z

yeah. according to chatGPT now that is done using something like this:

(ns example.d3
  (:require [reagent.core :as r]
            ["d3" :as d3]))

(defn d3-chart [{:keys [x y]}]
  (let [ref (r/atom nil)]
    (r/use-effect
      (fn []
        (let [node @ref]
          ;; either create or update chart here
          (-> (d3/select node)
              (.selectAll "circle")
              (.data [nil])
              (.join "circle")
              (.attr "r" 20)
              (.attr "fill" "steelblue")
              (.attr "cx" x)
              (.attr "cy" y)))
        js/undefined)
      ;; dependencies: re-run effect when x or y changes
      #js [x y])
    [:svg {:ref #(reset! ref %)
           :width 300
           :height 200}]))

2025-11-02T21:37:05.911709Z

Yes, useRef is a hook you can use to capture a node element into React, so you can use it idiomatically from within React (as opposed to document.querySelect . It also enables you to capture when that has actually rendered and is available for use, by you checking that the ref is not null . That said, the actual updating of a third-party outside-of-react library would have to work according to said library's docs, since they probably have their own update/destroy/create lifecycle functionality, unless they provide a react-component themselves as well.

2025-11-02T21:38:08.525259Z

You can of course remove the node from DOM and re-create it on every state change, but that's not very efficient.

borkdude 2025-11-02T21:38:11.014929Z

yes of course, this is why you need lifecycle methods 10 years ago. willUnmount etc.

2025-11-02T21:40:28.214679Z

Yup, so react will-unmount these days is also useEffect:

useEffect(() => {
  // do something on-mount
  
  return () => {
    // do something on-unmount i.e clean-up
  }
], []); <-- empty dependency array means it will only run once, on-mount. 
The returning function inside the useEffect ... effect function (it gets a bit dizzying) is the clean-up for the effect.

borkdude 2025-11-02T21:40:56.188519Z

ah ok

p-himik 2025-11-02T21:41:16.260089Z

Which, as I mentioned above, is still not a 100% alternative because it will be called multiple times in development. But not in production.

borkdude 2025-11-02T21:41:31.164789Z

(I still don't know why react introduced all this use stuff - those lifecycle methods worked fine??)

2025-11-02T21:41:53.615649Z

Because people like shiny things, and functional programming was that shiny thing 7 years ago for the JS community.

2025-11-02T21:42:24.458619Z

To the point where if you use classes in JS today, you get laughed at, despite being a completely normal part of the language.

borkdude 2025-11-02T21:42:41.882989Z

Did you know that :ref is also going to support a returned function that is called when the ref is unmounted?

2025-11-02T21:42:55.700249Z

I did not!

borkdude 2025-11-02T21:43:59.579229Z

or something like this, I think @chris358 linked me to some docs about this

๐Ÿ‘ 1
2025-11-02T21:44:02.947389Z

I should also say at this point that I am not a front-end guru, so there may be better patterns out there in use by react professionals than I outlined here.

borkdude 2025-11-02T21:44:03.534589Z

let me find it

p-himik 2025-11-02T21:44:10.173839Z

> I still don't know why react introduced all this use stuff - those lifecycle methods worked fine?? For simple use-cases. Without stuff like reactions in Reagent, before hooks it was harder to run something in React only when a particular property changes and also avoid recreating stuff that has already been created.

borkdude 2025-11-02T21:44:40.560139Z

basically what form-2 components did for reagent?

p-himik 2025-11-02T21:46:06.327959Z

Ehh, kinda, yeah. On the other hand, we now have hook hell. Lots of projects where people be like "oh, this is not just the view code, so I'll put it in a hook", and we get hook nesting, hook passing, dynamic hooks, all sorts of crap. Impossible to debug and reason about.

borkdude 2025-11-02T21:46:06.902309Z

ah here it is, the ref callback function: https://react.dev/reference/react-dom/components/common#ref-callback

2025-11-02T21:49:50.389299Z

> On the other hand, we now have hook hell. Lots of projects where people be like "oh, this is not just the view code, so I'll put it in a hook", and we get hook nesting, hook passing, dynamic hooks, all sorts of crap. Impossible to debug and reason about. Oh yeah, some modern React apps I've worked on have been pretty damn difficult to navigate, with hooks that lead to hooks that lead to hooks, with no real implementation to be seen anywhere, and no cohesive understanding of what triggers what. People love to go overboard with this stuff, though I don't see it being too different from over-complicated object oriented programming with classes, where much the same way inexperienced developers that sit in awe of complexity over-engineer everything. In both cases you need someone with experience and wisdom to keep the herd under control.

p-himik 2025-11-02T21:50:08.633229Z

Hook race conditions are fun. :D

borkdude 2025-11-02T21:50:10.583939Z

hmm > Unless you pass the same function reference for the ref callback on every render, the callback will get temporarily cleanup and re-create during every re-render of the component. > In the above example, (node) => { ... } is a different function on every render. When your component re-renders, the previous function will be called with null as the argument, and the next function will be called with the DOM node. This sucks a bit though. It means you have to hoist the function upwards.

borkdude 2025-11-02T21:50:48.049419Z

Is that the current behavior in reagent as well? I never realized that

p-himik 2025-11-02T21:51:45.310879Z

That's why there are useRef and useMemo. Reagent doesn't treat :ref as something special IIRC, so yeah, should behave the same. I usually hoist everything like that into r/with-let.

borkdude 2025-11-02T21:53:21.795219Z

I guess I could just change that rule in my lib :ref seems useful but re-calling it on every render is a bit too much maybe? unless... you want did-component-update ๐Ÿ’ก

p-himik 2025-11-02T21:57:50.580389Z

For that, modern code uses useEffect. Re-calling the ref function makes sense if it changes.

borkdude 2025-11-02T21:58:25.200369Z

it changes by nature. if you re-call a function a new inner function gets created

p-himik 2025-11-02T22:01:10.384339Z

Yes, hence useMemo. And a ref function can be changed deliberately - that's why re-calling it makes sense.

borkdude 2025-11-02T22:04:41.861509Z

so you use memo to pass it a function that returns the ref function? yeah I see

borkdude 2025-11-02T22:05:09.708929Z

thanks for the update on hooks. so far I managed to avoid them but it's good to know about them

borkdude 2025-11-02T22:05:26.480949Z

well I've used useState etc in a few squint demos just to show they worked

borkdude 2025-11-02T22:05:38.813209Z

but that's probably the only one

borkdude 2025-11-02T23:07:13.060979Z

I now have this working, inspired by replicant. The nested render is just to demonstrate what you would normally do with an uncontrolled JS thing.

(ns main
  (:require
   ["../../reagami.mjs" :as reagami]))

(def state (atom {:counter 0 :show true}))

(defn sub-component [x]
  [:div "Counter in subcomponent " x])

(defn ui []
  [:div#ui
   [:button {:on-click #(swap! state update :show not)}
    "Show? " (:show @state)]
   (when (:show @state)
     [:div
      [:pre (pr-str @state)]
      [:div#my-custom {:on-render (fn [node lifecycle]
                                    (case lifecycle
                                      (:mount :update) (reagami/render node [sub-component (:counter @state)])
                                      :unmount (prn :unmount)))}]
      [:button {:on-click #(swap! state update :counter inc)}
       "Click me!"]])])

(defn render []
  (reagami/render (js/document.querySelector "#app") [ui]))

(add-watch state ::render (fn [_ _ _ _] (render)))

(render)
So you just have :on-render and it gets the node + lifecycle which can be :mount, :update or :unmount

borkdude 2025-11-02T23:09:26.377509Z

so every time you click, the main component updates (because of add-watch) but the sub-component also updates (because of a nested re-render). when you click show, the nodes are destroyed, but when you click show again, the nodes come back alive again.