fulcro 2025-09-01

@tony.kay according to Claude:

React 19's concurrent rendering fundamentally changes when component props are updated relative to state changes. The key issue is that props updates are now asynchronous and batched, whereas Fulcro code often expects them to be synchronous.

  1. create-element (components.cljc:720-740) - Creates React elements with props wrapped in a JS object:
  props #js {:fulcro$value   props
             :fulcro$queryid qid
             :fulcro$parent  parent
             :fulcro$app     app}
  2. isoget-in and props function (raw/components.cljc:55-67) - Retrieves props from React components:
  (defn props [component]
    (let [props-from-parent    (isoget-in component [:props :fulcro$value])
          computed-from-parent (get-computed props-from-parent)
          props-from-updates   (computed (isoget-in component [:state :fulcro$value]) computed-from-parent)]
      (newer-props props-from-parent props-from-updates)))

  The issue is that isoget-in reads from the React component's props/state, which in React 19 may not be updated yet due to concurrent rendering.

released rc8

The idea of using useSyncExternalStore is interesting, and maybe not even that hard since raw components are handling a lot now. but for this to work every component would need to do it right? that means props will no longer pass from the parent but would come through SyncExternalStore? Then that changes a lot?

I think I can embed an auto-wrapper at mount

I’ve got it coded…working on making it work 😄

making each component subscribe to their own query with useSyncExternalStore?

no no…top-level root only

Root depends on state-map

state-map is the source of all data of the entire tree…state map changes->root render

Try commit 7edbd1b214ed424bd03e01db31d9e8aba717a839

what if we just don't use the react state outside of components? isn't it the only place where that's a real issue? so my rad patch was incorrect but it's just a matter of properly denormalizing there? I feel like although there is a performance impact it is actually not necessarily bigger than whatever needs to happen for the react state to synchronize?

that adds in a seamless wrapper that should fix it…it isn’t optimal because it does a bit of over-rendering, but I want to see if that resolves your issues

no, the issue is that Fulcro changes it’s own state asynchronously, and react can’t properly detect when that is happening, so it gets out of sync. Using the hook gives us a way to teach it when to re-render based on our state changes.

There’s a bit more work to make this optimal…a bunch of my “rendering optimizations” are going to be basically impossible now (I think?). So all of the different “pluggable renderers” are now obsolete.

The commit I just sent will basically force react to render any time the atom changes. I can make that better, but it’s a deeper change.

it fails to compile with fulcro-18n

what is the error

I didn’t think I made any external visible changes

RuntimeException: No such var: app/force-root-render!

I didn’t remove any functions

restart shadow?

I don’t understand

it’s still there

yea I deleted all build artifacts and now I get RuntimeException: No such namespace: react looking into it

juts a min…let me fix that 😕

965cbdcd114bda6d27ad5e4ed9260dc263aafcd9

hold on 😕

i fixed it locally

6d1856890e8f6581c5eb96ef5d79eae39b4a3db9

it compiled but app doesn't work anymore, stuck on loading

you have cache disabled

ERROR [com.fulcrologic.fulcro.routing.dynamic-routing:1468] -  Routing failed. Unable to construct route path from given arguments 
{:router nil, :target :brian.ui-components.classes/YourClasses}

and console open?

oh….it’s possible that the app-root wrapper isn’t as transparent as I wanted 😞

try ff5d73928d8bbf9b58d97b589b50f06b20b9dfc4

basically branch sync-external-store if you’re using a checkout of fulcro source

it loads

I see 2 errors in console: application.cljc:344 Cannot update a component (`com$fulcrologic$fulcro$application$FulcroRootWrapper`) while rendering a different component (`brian.ui-components.chat/Chat`). To locate the bad setState() call inside brian.ui-components.chat/Chat, follow the stack trace as described in https://react.dev/link/setstate-in-render overrideMethod @ hook.js:608 scheduleUpdateOnFiber @ react-dom-client.development.js:14382 forceStoreRerender @ react-dom-client.development.js:6126 eval @ react-dom-client.development.js:6111 eval @ application.cljc:344 eval @ core.cljs:4538 cljs$core$reset_BANG_ @ core.cljs:4579 eval @ core.cljs:4615 com$fulcrologic$fulcro$raw$application$maybe_merge_new_component_BANG_ @ application.cljc:491 com$fulcrologic$fulcro$react$hooks$use_component_$initializecomponent_state @ hooks.cljc:343 mountStateImpl @ react-dom-client.development.js:6132 mountState @ react-dom-client.development.js:6153 useState @ react-dom-client.development.js:22953 exports.useState @ react.development.js:1223 com$fulcrologic$fulcro$react$hooks$use_state @ hooks.cljc:44 com$fulcrologic$fulcro$react$hooks$use_component @ hooks.cljc:339 com$fulcrologic$statecharts$integration$fulcro$hooks$use_statechart @ hooks.cljc:36 eval @ chat.cljc:497 com$fulcrologic$fulcro$components$wrapped_render @ components.cljc:692 brian$ui_components$chat$render_Chat @ chat.cljc:481 brian$ui_components$chat$Chat @ chat.cljc:481 react-stack-bottom-frame @ react-dom-client.development.js:23865 renderWithHooks @ react-dom-client.development.js:5531 updateFunctionComponent @ react-dom-client.development.js:8899 beginWork @ react-dom-client.development.js:10524 runWithFiberInDEV @ react-dom-client.development.js:1521 performUnitOfWork @ react-dom-client.development.js:15134 renderRootSync @ react-dom-client.development.js:14958 performWorkOnRoot @ react-dom-client.development.js:14421 performSyncWorkOnRoot @ react-dom-client.development.js:16233 flushSyncWorkAcrossRoots_impl @ react-dom-client.development.js:16081 processRootScheduleInMicrotask @ react-dom-client.development.js:16118 eval @ react-dom-client.development.js:16252 <brian.ui-components.chat/Chat> exports.createElement @ react.development.js:1035 eval @ core.cljs:4001 eval @ core.cljs:4043 com$fulcrologic$fulcro$components$create_element @ components.cljc:741 com$fulcrologic$fulcro$components$element_factory__delegate @ components.cljc:790 com$fulcrologic$fulcro$components$element_factory @ components.cljc:750 eval @ core.cljs:2085 eval @ ui.cljc:180 eval @ ui.cljc:180 com$fulcrologic$fulcro$components$wrapped_render @ components.cljc:692 brian$ui$render_Root @ ui.cljc:141 eval @ core.cljs:3995 eval @ core.cljs:4034 G__65185__delegate @ components.cljc:381 G__65185 @ components.cljc:375 react-stack-bottom-frame @ react-dom-client.development.js:23878 updateClassComponent @ react-dom-client.development.js:9456 beginWork @ react-dom-client.development.js:10538 runWithFiberInDEV @ react-dom-client.development.js:1521 performUnitOfWork @ react-dom-client.development.js:15134 renderRootSync @ react-dom-client.development.js:14958 performWorkOnRoot @ react-dom-client.development.js:14421 performSyncWorkOnRoot @ react-dom-client.development.js:16233 flushSyncWorkAcrossRoots_impl @ react-dom-client.development.js:16081 processRootScheduleInMicrotask @ react-dom-client.development.js:16118 eval @ react-dom-client.development.js:16252 <brian.ui/Root> exports.createElement @ react.development.js:1035 eval @ core.cljs:4001 eval @ core.cljs:4043 com$fulcrologic$fulcro$components$create_element @ components.cljc:741 com$fulcrologic$fulcro$components$element_factory__delegate @ components.cljc:790 com$fulcrologic$fulcro$components$element_factory @ components.cljc:750 eval @ core.cljs:2085 com$fulcrologic$fulcro$application$FulcroRootWrapper__delegate @ application.cljc:363 com$fulcrologic$fulcro$application$FulcroRootWrapper @ application.cljc:358 react-stack-bottom-frame @ react-dom-client.development.js:23865 renderWithHooks @ react-dom-client.development.js:5531 updateFunctionComponent @ react-dom-client.development.js:8899 beginWork @ react-dom-client.development.js:10524 runWithFiberInDEV @ react-dom-client.development.js:1521 performUnitOfWork @ react-dom-client.development.js:15134 renderRootSync @ react-dom-client.development.js:14958 performWorkOnRoot @ react-dom-client.development.js:14421 performWorkOnRootViaSchedulerTask @ react-dom-client.development.js:16218 performWorkUntilDeadline @ scheduler.development.js:46 <com$fulcrologic$fulcro$application$FulcroRootWrapper> exports.createElement @ react.development.js:1035 eval @ dom.cljs:106 cljs$core$apply_to @ core.cljs:3971 eval @ core.cljs:4041 G__78741__delegate @ react_interop.cljc:24 G__78741 @ react_interop.cljc:22 eval @ keyframe_render.cljc:54 com$fulcrologic$fulcro$rendering$keyframe_render$render_BANG_ @ keyframe_render.cljc:51 eval @ keyframe_render2.cljc:40 eval @ application.cljc:285 eval @ application.cljc:111 reset_mountpoint_BANG_ @ application.cljc:375 eval @ application.cljc:384 brian$client$init @ client.cljs:131 eval @ shadow.module.main.append.js:4 goog.globalEval @ main.js:451 env.evalLoad @ main.js:1408 (anonymous) @ main.js:4086Understand this error application.cljc:344 Cannot update a component (`com$fulcrologic$fulcro$application$FulcroRootWrapper`) while rendering a different component (`brian.ui-components.classes.course-creation-modal/CourseCreationModal`). To locate the bad setState() call inside brian.ui-components.classes.course-creation-modal/CourseCreationModal, follow the stack trace as described in https://react.dev/link/setstate-in-render

so yeah I guess I have to patch the whole rendering pipeline

it fixes the issue with rad-routing

oh, so the console errors don’t crash it

yea doesn't crash, but it feels slower

yes, it’s overrendering and probably logging errors

I didn't change the course in the video but actually when doing so the UI flashbing before the blank is the previous course

that sentence was word salad to me ^^^

you’re saying that it is showing the prior course as a flicker? In the before or after?

yes I'm saying that when I navigate to a course, the view flickers and what shows up for a brief instant before a blank is the view of the previous course, then comes a blank then the current course. as shown in the video above

and you’re using deferred routing?

it shouldn’t change the ident…

or this is a RAD form?

it's a rad form

(defmutation route-to-class [{:keys [id]}]
  (action [{:keys [app]}]
          (uism/trigger! app :ui/current-course
                         :event/will-enter-with-ident
                         {:course-ident [:course/id id]})
          (form/edit! app class/ClassContent id)))

how about this: 3de94d8cfe42fa456051768781339cfc3fe20133

what is the extra “trigger” for? RAD does routing to forms properly…I suspect you’re adding in some kind of fancy side routing?

The big downside of this react hook is that it ends up forcing me to do a root render often…not sure how much that is affecting overall performance

I actually just removed that extra trigger, but I overwrite the form will-enter:

(defn form-will-enter [get-form-class]
  (fn [app {course-id :course/id :keys [action id] :as route-params}]
    (let [form-class (get-form-class)
          {::attr/keys [qualified-key type]} (comp/component-options form-class ::form/id)
          new? (= form/create-action action)
          coerced-id (if new?
                       (tempid/tempid id)
                       (ids/id-string->id type id))
          course-ident (when course-id [:course/id course-id])
          form-ident [qualified-key coerced-id]]
      (when (and new? (not (ids/valid-uuid-string? id)))
        (log/error (comp/component-name form-class)
                   "Invalid UUID string " id "used in route for new entity. The form may misbehave."))
      (dr/route-deferred
       form-ident
       (fn []
         (log/info "form will enter " course-ident " " form-ident " " form-class)

         (uism/trigger! app :ui/current-course
                        :event/will-enter-with-ident
                        {:course-ident course-ident
                         :form-ident form-ident
                         :class form-class})
         (form/start-form! app coerced-id form-class route-params))))))
the reason is that I set the :root/current-course in that event handler and do a bunch of loads

so the flickering is probably your changes…deferred routing won’t change the ident on the router until you say it is ready. Any render frame is a pure fn of the state-map…so any version of the state map you create could be rendered. If you allow an ident to appear in state that isn’t what you want (is a prior thing, or a blank thing, etc.) then you have to deal with the fact that it could be rendered, even if it is transient. This is the prime simplification and promise of Fulcro: Your view is exactly what your state says it should be, clamped to some kind of max frame rate.

The deferred routing feature of the dr namespace helps address this: You can tell it exactly when it is safe to change the state map so that the router points to the new thing

but I am using the deferred routing here?

it don't have the flicker anymore with your last commit

ah, so with the over-rendering, the intermediate swaps on the atom were rendering states that were not intended to be rendered

this is why Fulcro normally doesn’t watch the atom…a single “atomic” transaction (optimistic) should result in a render once that transaction is done.

so yeah, that explains why you saw the problem

The correct picture Fulcro attempts to give is that the state map moves from one time to another via a txn…but optimistic actions can do multiple swaps. So, by rendering on a watched atom I was breaking the atomic promise of render per txn.

so, barring a bit of cleanup and testing, that is probably the basic patch that is needed. I’m curious if I can get back the optimized targets and if the tunneled props still maintain the optimizations…perhaps it is closer than I hope…

actually the !! variant of props edit still seem to work correctly

so that’s the most important optimization for making forms fast…and we still have memoized props and shouldComponentUpdate in legacy defsc….so, it’s probably all good. I just need to modify all of the pluggable renderers to do the right thing (possibly?).

Are you still getting console errors on the first render?

good…perhaps it is a reasonable patch then

I just tried the patch out on my large app, and it seems ok

I’ll roll out a new RC

ok I'll update and keep testing

Thanks for the report and the help

@tony.kay seems like there's some issues with changing the language now

it's like they get stalled

Ah, force root render isn’t overriding the shouldcomponentupdate short-circuit…I don’t remember how that works, but it should be an easy fix.

I don’t have time to look at it this morning…but basically I think calling the optimized-render instead of just the ses render when force root is set will do it.

Basically all defsc components define a shouldComponentUpdate that prevents re-render if the props didn’t change, but of course on a locale change you need it to force past that…the shouldComponentUpdate code that is generated has a built-in condition for that

in c.f.f.component.cljc

The fix should be done in c.f.f.r.applicaation.cljc

I added this cond to pick the new rendering over core rendering always…but it should use core-render when forcing a refresh

OR OPTIONALLY you could enable forced root manuall

ah…that was easy…fixed and pushed…try it out

thanks just tried and seems to work

pushing it a bit more:

The root cause is React 18's concurrent rendering model, which React 19 continues to use. The fix for Fulcro needs to account for the fact that since React 18, component props are updated asynchronously after state changes when using createRoot. This is why reading directly from app state (which is updated synchronously in Fulcro) works, while reading from component
  props (which React updates asynchronously) can give stale values.

Well, that probably means a whole lot of things broke

But your patch isn't quite right. Form props are a tree. You're getting just the normalized top entity

I wonder if there's a way to delay action until react finishes render in general. That would restore the semantics. Otherwise applications are going to break in all sorts of random ways

> I wonder if there's a way to delay action until react finishes render in general. now that I think about it, I have add a few mysterious bugs in the past year where switching from this to app when sending events to state machine for instance fixed the issue. I'm wondering if it is related

You're getting just the normalized top entity
doesn't look like it, com.fulcrologic.fulcro.algorithms.form-state/config is a map and I Have the pristine-state in it, in normalized form it is in :com.fulcrologic.fulcro.algorithms.form-state/forms-by-ident ok makes more sense maybe some code was stale I do see idents now

ah, that looks more promising…does that seem to fix your issue? It still looks non-deterministic to me, but from my reading so far, IF you end up in the microtask queue AFTER react, then it should be ok.

would be nice if there was a task priority support so we could specify we go after react always 😕

I think perhaps using this is the proper way to deal with this now: https://react.dev/reference/react/useSyncExternalStore or useEffect in a top-level wrapper.

Usage 
Subscribing to an external store 
Most of your React components will only read data from their props, state, and context. However, sometimes a component needs to read some data from some store outside of React that changes over time. This includes:

Third-party state management libraries that hold state outside of React.

this would actually be an improvement over the setTimeout historical thing we’re doing…because React can decide what/when to render and we just tell it when the data has changed.

and I think I left a hook for “schedule a re-render” which defaults to next animation frame, but instead could be the call to the callback that tells react we’re ready to render again.

Do you want to try your hand at that, or do you want me to try it?

you have the failing cases, but I could probably patch it together faster

Ican help reproduce with a minimal case

based on my current understanding of fulcro and react internal which isn't much the main culprit for my bug is the use of (comp/props ...) in mutations because it uses the derived react state not the app state so my patch (just tested after recompiling) is hack that works around that by waiting for react to update but that's just a workaround since it defeats the purpose of the underlying react changes.

Right, so I think if I change the transaction processing engine to notify react when the state map changes, then that will force react to fix the UI, which in turn fixes the props you’re relying on. Ultimately the problem is that the UI props are “stale”, indicating that React decided a re-render wasn’t necessary.

That's worth a try yeah