i wrote a blog post https://jyn.dev/build-system-tradeoffs/
learnt something from it. thanks for sharing!
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... ๐งต
Respond in thread
โข :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.
would it be a problem for you if you only had did-unmount instead of will-unmount?
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.
yes, the original node will be passed but it won't be connected to the DOM anymore
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
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.
oh yeah you need did update whenever a JS component must change according to some argument given to a component
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.
I'm not even thinking about components yet, nor classes. Just hiccup -> dom nodes
(similar to replicant I guess)
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.
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?
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).
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.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.
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.here the useState , useEffect being built-in React hooks, not custom made code.
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]))
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?
So you mean that D3 is not rendered with React at all?
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}]))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.
You can of course remove the node from DOM and re-create it on every state change, but that's not very efficient.
yes of course, this is why you need lifecycle methods 10 years ago. willUnmount etc.
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.ah ok
Which, as I mentioned above, is still not a 100% alternative because it will be called multiple times in development. But not in production.
(I still don't know why react introduced all this use stuff - those lifecycle methods worked fine??)
Because people like shiny things, and functional programming was that shiny thing 7 years ago for the JS community.
To the point where if you use classes in JS today, you get laughed at, despite being a completely normal part of the language.
Did you know that :ref is also going to support a returned function that is called when the ref is unmounted?
I did not!
or something like this, I think @chris358 linked me to some docs about this
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.
let me find it
> 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.
basically what form-2 components did for reagent?
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.
ah here it is, the ref callback function: https://react.dev/reference/react-dom/components/common#ref-callback
> 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.
Hook race conditions are fun. :D
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.
Is that the current behavior in reagent as well? I never realized that
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.
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 ๐ก
For that, modern code uses useEffect.
Re-calling the ref function makes sense if it changes.
it changes by nature. if you re-call a function a new inner function gets created
Yes, hence useMemo. And a ref function can be changed deliberately - that's why re-calling it makes sense.
so you use memo to pass it a function that returns the ref function? yeah I see
thanks for the update on hooks. so far I managed to avoid them but it's good to know about them
well I've used useState etc in a few squint demos just to show they worked
but that's probably the only one
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 :unmountso 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.