reagent

2025-07-05T12:21:05.582599Z

I’m testing Reagent 2.0.0-alpha1 and followed the upgrade steps in the README. Everything works fine, except for live reloading. Our render setup looks like this:

(defonce react-root (delay (rd/create-root (.getElementById js/document "app"))))

(defn ^:dev/after-load mount-root []
  (rf/clear-subscription-cache!)
  (rd/render @react-root [root/component]))
It renders fine initially, but after change some components, any state-updates aren’t re-rendered. So it’s essentially a static page. I can see the re-frame state changing to something new by checking internally, but this isn’t reflected in a new UI update. However, if I wrap root/component in a var like this:
(rd/render @react-root [#'root/component])
then hot reload works. Any idea why this happens? Feels like there’s an underlying issue that shouldn’t require the var workaround.

juhoteperi 2025-07-14T09:15:25.011779Z

So the problem with this optimization/simplified impl is that with: • one ratom • one reaction using the ratom value • one component using both the ratom value and the reaction value changing the ratom value once, will trigger component render twice, because the ratom change has time to trigger the render before reaction is updated and then that will trigger the render again.

juhoteperi 2025-07-14T09:16:27.797319Z

^ Notes about trying out removing of one of Reagent queues in this thread Performance is promising but it might break the Reagent's promise of ratom/reaction changes triggering only one render

thheller 2025-07-14T09:34:26.571039Z

isn't the whole deal with react19 and async rendering that you can't guarantee "one render" rendering everything anyway?

juhoteperi 2025-07-14T09:35:54.593379Z

it is possible with flushSync, but might be a bad idea

juhoteperi 2025-07-14T09:36:44.704219Z

they do strongly recommended not using it, but their docs aren't obviously for cases like Reagent where we are already queuing the changes ourselves

juhoteperi 2025-07-14T09:38:43.760209Z

the async rendering is pretty bad for some ratom update cases, instead of the change being shown after 16ms, it will take 32ms to DOM being updated

juhoteperi 2025-07-14T09:39:14.923059Z

(select-row benchmark on the first run, before I added flushSync)

juhoteperi 2025-07-14T09:40:44.974279Z

adding the flushSync to ratom change handling doesn't affect async rendering with state hooks or any other state sources

juhoteperi 2025-07-14T10:28:04.270159Z

And the "two component" render problems in that ratom example isn't really about React rendering stuff multiple times, but about changing the Reagent ratom queue so that it causes Reagent to tell React about the changes twice. Vs. the current 4 queues (separate ratom and component queues), where Reagent will end up telling React about the changes only once.

juhoteperi 2025-07-14T10:28:28.590749Z

So the React 19 rendering possibly splitting the rendering into multiple frames isn't even relevant for this case.

2025-07-06T08:00:10.580069Z

Ah, so it’s not an isolated case. Good to know! I’m happy to help debugging the alpha version further. So feel free to ping me. Our reagent frontend is around 50k LOC. So it’s quite decent in size and probably covers all kinds of edge-cases which could help with debugging.

juhoteperi 2025-07-10T07:38:18.664189Z

I'll check what is the deal with live reloads and update docs or the impl. I think I want the reagent API to work without any of these workarounds (react key or vars.)

juhoteperi 2025-07-10T07:39:58.949479Z

I think I used some tricks on the tests to run code after React is done with rendering, I might have to add the same to the after-render queue flushing.

juhoteperi 2025-07-10T07:40:55.594849Z

Though for tests my solution is just waiting for 17ms which isn't great for after-render.

juhoteperi 2025-07-10T07:42:32.815319Z

Maybe I can use flushSync in afterRender to force React to flush its queue before flushing Reagent queue. Not perfect but might be the only option.

2025-07-10T07:49:37.875689Z

Sounds good! You could consider useEffect maybe. It runs after things are committed to the dom. There might be some possibilities there. Although, it needs to added to the component-hierarchy, and it only runs when that specific component re-renders. As far as I know that’s the only way to know react is done rendering, or by forcing it with flushSync, like you said.

juhoteperi 2025-07-10T07:50:29.943459Z

There is likely no way to use hooks due to how Reagent API works

2025-07-10T07:50:56.282759Z

ah, check

juhoteperi 2025-07-10T07:51:02.591959Z

One option is to just remove after-render completely, and provide documentation how to use hooks instead

juhoteperi 2025-07-10T09:15:24.374829Z

Interestingly the live reload works for Reagent examples. Could be because those are just one namespace.

juhoteperi 2025-07-10T09:18:23.360309Z

Yes

juhoteperi 2025-07-10T09:18:41.185909Z

I'm not sure if this is 2.0 difference

juhoteperi 2025-07-10T09:23:17.878619Z

If your root component is in different namespace than render call, shadow-cljs doesn't by default reload all the transitive dependents of the changed namespace. You change the root ns -> root js file is loaded again and new component fn is evaluated BUT the namespace with render call isn't evaluated again so the vector in the old mount-root call still refers to the old fn value. Var fixes this by adding indirection so that the Reagent will always resolve the current value on render time. :reload-strategy :full also fixes this, as it makes shadow-cljs eval all the transitive dependents of the changed ns.

valtteri 2025-07-10T09:24:07.217969Z

Mmhmhmh but hot reloading works fine in Lipas master?

juhoteperi 2025-07-10T09:24:21.191489Z

I don't understand your key hack. It doesn't make sense.

valtteri 2025-07-10T09:24:39.829179Z

Did you experience the problem that came without it?

juhoteperi 2025-07-10T09:25:17.288389Z

And hm lipas master works indeed with the old Reagent and without reload-strategy full.

valtteri 2025-07-10T09:25:59.786039Z

I don't understand why my hack works either.. 😄

valtteri 2025-07-10T09:26:26.791859Z

But that's how I got it to re-render.

juhoteperi 2025-07-10T09:26:36.942789Z

Hmhmhm. Not evaluating the dependent ns (the ns with render call) should be fine because fns in Cljs just compiles the fn to JS value... so the var indirection shouldn't be necessary.

juhoteperi 2025-07-10T09:27:43.891319Z

The [root/component] should just refer app.ns.root.component JS value (Closure modules etc.) so even without evaluating the core ns it should get the latest version of the component fn always.

juhoteperi 2025-07-10T09:28:50.170849Z

Buuuut maybe there is some fn comparison logic in React which causes it to use the previous fn if it thinks it didn't change.

juhoteperi 2025-07-10T09:28:56.002499Z

OR Reagent as-element!

juhoteperi 2025-07-10T09:29:11.484559Z

which turns the [root/component] into Reagent component

juhoteperi 2025-07-10T09:31:43.364999Z

The difference could be due this wrapper: https://github.com/reagent-project/reagent/blob/master/src/reagent/dom/client.cljs#L22-L43

juhoteperi 2025-07-10T09:52:22.739479Z

Looks like using createElement on line 31 works

juhoteperi 2025-07-10T09:52:24.238089Z

Not sure why

juhoteperi 2025-07-10T09:53:36.555839Z

It kind of makes sense the "as-element wrapper component" created in render fn should be used as a react element instead of a regular fn inline in the initial-flush-render wrapper component

juhoteperi 2025-07-10T09:53:48.586749Z

Maybe I can refactor it to avoid two wrapper components

juhoteperi 2025-07-10T10:27:50.295139Z

https://github.com/reagent-project/reagent/commit/817456f10fc6d5d4029ee1688c96da430cee7526 This hopefully makes live reloads work better. Available as a snapshot now: reagent/reagent {:mvn/version "2.0.0-SNAPSHOT"}

🙌 2
juhoteperi 2025-07-10T12:10:03.102259Z

@stex do you have some cases where it looks like after-render is called later than before, i.e. before React has mounted updated elements into DOM? I did try very quick case of adding after-render callback to the intro page "The atom click-count has value:" component where it just prints the current atom value and DOM value, and it seems the latest atom value is already in DOM here.

juhoteperi 2025-07-10T12:10:28.957109Z

hmm

juhoteperi 2025-07-10T12:10:52.185169Z

moving the after-render to button on-click on that component does show that atom value is updated, but dom still has the old value

juhoteperi 2025-07-10T12:11:05.818799Z

I guess it makes sense

juhoteperi 2025-07-10T12:12:33.345509Z

If the after-render callback is only added at the render of the reagent component, react has already the update queued, and after-render queue and react dom mount likely take the same 1 animation frame. Adding the after-render callback outside of the render does cause it really to wait for Reagent to flush updates from on-click ratom changes, but then it React will take additional time.

2025-07-10T12:13:28.295509Z

I have one specific case in our app. But i haven’t isolated that one yet. But it seemed obvious to me that this was due to the scheduling of react being different now. I think what you’re saying now confirms that. reagent waits, calls the functions, but than it takes some additional time by react to render.

2025-07-10T12:14:41.283779Z

Our use-case is a bit of hack. We call r/after-render to reset the scroll position to the top of the page when you navigate to another page. Although hacky, that has worked for us for many years now. When updating the reagent alpha you could clearly see the the browser scrolling to the top on the old page (just a flash) and then rendering the new page.

juhoteperi 2025-07-10T12:14:52.383809Z

juhoteperi 2025-07-10T12:15:12.369089Z

I would likely recommend useEffect hook for such cases

2025-07-10T12:16:46.093319Z

Yeah, we will migrate to that. We’ve been hesitant to use useEffect with react 17 and the old reagent. Still a bit confused what the performance penalty is for using functional components with reagent. Also not sure how that is for the alpha version. But we opted to avoid it until we have time to profile it.

2025-07-10T12:17:39.173239Z

I think this page indicated there was some penalty. https://github.com/reagent-project/reagent/blob/master/doc/ReagentCompiler.md. I guess to use useEffect you have to use :f> .

juhoteperi 2025-07-10T12:17:40.887469Z

Slow part is turning the hiccup-like data to React elements on runtime

juhoteperi 2025-07-10T12:18:18.273369Z

Yes I think the way ratom updates are triggered for functional components vs the class based components is a bit slower

juhoteperi 2025-07-10T12:18:30.014789Z

due to functional components having to kind of re-implement class component API...

juhoteperi 2025-07-10T12:19:03.615659Z

BUT that benchmark was also with React 16, it is possible the React hook performance is better now

2025-07-10T12:19:50.023779Z

but it’s probably fine if you just sprinkle some :f> i guess. And I’m also curious what react 19 does yeah. Alpha reagent “felt” faster with 19, not sure if it’s placebo.

juhoteperi 2025-07-10T12:23:13.880779Z

I could update (at least my own branch) of the benchmark repo to compare a few reagent and react versions

2025-07-10T12:24:58.791829Z

That would be useful for us. But please only do so if you think it’s beneficial for others also. We need to profile anyway when we’ll be upgrading in production.

juhoteperi 2025-07-10T14:17:09.192059Z

There are some interesting results here. Like why select-row is so much slower for reagent-react-19. The differences for 1.3 and 2 fn and non-fn are pretty small in this benchmark.

juhoteperi 2025-07-10T14:18:06.951619Z

not sure why uncompressed size is also so much larger for reagent-react-19 versions

juhoteperi 2025-07-10T14:27:03.367569Z

huh, react-dom/client is just that much larger vs react-dom

2025-07-10T18:05:51.866609Z

Cool to see the benchmarks. Seems like you got a ways faster laptop recently 😉. Differences (except for select row) aren’t that big indeed 👍

juhoteperi 2025-07-10T18:06:39.290009Z

The repo benchmarks are 5 years old so they are 2 or 3 laptops ago 😄

juhoteperi 2025-07-10T18:07:02.150449Z

The benchmark setup itself could also have changed a bit

2025-07-05T12:37:53.057389Z

Sidenote: Not sure where to report this. I think reagent/after-render has different behaviour now with React 19. My guess is that this is being called after the request animation frame of Reagent, but React also does some scheduling, so it’s likely still before the React renderer is done.

valtteri 2025-07-05T15:23:44.756019Z

Same problem, but I found a semi-ugly workaround https://github.com/lipas-liikuntapaikat/lipas/pull/160#issuecomment-3009612002 Juho is currently on holiday, but he'll look into it once he's back. 🌴

valtteri 2025-07-05T15:24:47.358819Z

And thanks for testing the alpha! All testing and feedback is helpful.

valtteri 2025-07-05T15:26:23.727449Z

That var trick is actually nicer than my react-key hack. Dunno why it works though and the "normal case" doesn't.

juhoteperi 2025-07-11T09:18:07.216339Z

Interesting, I can get better results (especially for select-row) by adding flushSync call to where Reagent flushes its ratom change queue to React. Might be fine, it is once per animation frame, only when you are making ratom changes. It also has affect that after-render callback really sees the changes mounted to DOM.

juhoteperi 2025-07-11T09:40:33.503859Z

https://github.com/reagent-project/reagent/pull/630/files

juhoteperi 2025-07-11T10:14:20.414259Z

Some batching notes: Reagent has 4 queues that are flushed on each animation frame: 1. before-flush queue 2. ratom changes (reaction change triggering dependent reactions) 3. component render 4. after-render Ratom/reaction changes are very dependent on having batching, so the requestAnimationFrame queueing can't be fully removed from Reagent. From point of view of 2. ratom queue, every component render body is also a reaction! This is how component renders catch which reactions/ratoms are used within render body. On render reactions, there is code that also adds the component to queue 3. when dependencies for that reaction changed. It is important that the ratom queue is handled first during the animation frame, before component renders, so we can't remove any one of these queues alone, even though React now queues the renders itself. Maybe. I think. This flushSync solution avoids the additional React queue for component renders, because now Reagent forces React to flush all those component changes right-away into DOM. ... Now that I'm writing this out, I'm starting to wonder if it would still be possible to remove 3. component queue, if 2. ratom queue just directly triggered the React updates. BUT then the after-render queue would be problematic.

juhoteperi 2025-07-11T10:27:10.569559Z

3. component render queue is also ran in component mount order, to ensure parent components are rendered first. This ensures no component is rendered multiple times, because rendering the children from the parent render will mark them clean, and then they aren't rendered again even if the children was also added to the queue by itself. This might be why I'm seeing some tests break when testing this idea. For example: there is a ratom change that triggers both parent and child render, if child is rendered first, and then parent is rendered, also rendering the children again, the children gets rendered one extra time.

juhoteperi 2025-07-11T10:28:00.794539Z

Or maybe the React itself is wise enough to handle those cases...

juhoteperi 2025-07-11T10:28:15.938699Z

The test failures could be something else.

juhoteperi 2025-07-11T10:53:12.328539Z

Just removing 3. queue from Reagent has the same problem then as without flushSync in flushing that queue. Then the ratom changes in 2. queue will add component renders into React queue and there is again double batching for component renders.

juhoteperi 2025-07-11T11:05:24.520789Z

Adding the flushSync around 2. ratom queue flush could work.

juhoteperi 2025-07-11T11:05:35.704829Z

This looks quite promising. Same perf as old reagent with react 16.

👏 1
💪 1