Hello,
We use the TreeEngine in our application. By default, when expanding a node that needs to load data (because the node was expanded for the first time), a ProgressIndicator is shown in the row.
We now have the problem that, due to some change/further development on our side, this ProgressIndicator is no longer displayed, and unfortunately we can no longer trace which change caused it.
Here’s what I’ve found so far: The RowProgressIndicator is still being used, but rowContext.rowState.uiState.busy no longer switches to true. It also doesn’t seem to be related to any ComponentMap or WidgetMap changes. Since we only use the A12 client, we have our own TreeEngineDataLoader. However, it was still working when we initially introduced this DataLoader.
Can anyone give me a hint on what I could look for on my side that might cause busy to no longer take the value true?
Thanks in advance.
We hit the exact same symptom (expanding a node takes a moment, child rows appear, but the per-row spinner never shows) and tracked it down, so here’s the full trail in case it helps others.
How the row spinner works
The RowProgressIndicator renders only while its row’s busy === true, which the TreeEngine derives from the corresponding data holder’s busy flag. That flag goes true when the load starts and false when the data is set. So the spinner is only visible if the browser actually paints a frame during that busy window.
Possible causes we considered (in order)
- Missed render tick / store-update timing .. the busy update getting coalesced away.
- Synchronous / in-memory loader .. an instant load whose busy window is shorter than one paint frame (~16 ms), so it never composites.
- Heavy view rendering after the data arrives .. busy is already false by then, so the spinner can’t cover it.
- A long synchronous task on the click itself, before the loader even runs.
What it actually was
Redux DevTools first told us the theories 1-3 were wrong: busy: true was reaching the store, and stepping through states slowly did render the spinner .. so the state → selector → row-context → indicator → paint pipeline was healthy. The problem was that nothing ever painted while busy was true.
A browser performance profile of a single expansion click was decisive. The entire ~1 s freeze was one unbroken synchronous task inside the click handler .. before the data provider ran at all:
handleArrowButtonClick -> onNodeExpansionChanged -> dispatch
-> react-redux notify / checkForUpdates
-> <a selector>
-> <a type-guard>
-> zod safeParse over the full dataset
~90% of the freeze was a type-guard doing a full schema validation of the entire dataset, called from inside a Redux selector. Because it sat in a selector, it re-ran on every store notification during the expansion (effectively per row/cell), blocking the main thread synchronously.
The key point: this task does not contain the load cycle — it sits upstream of it and delays it. The load cycle proper (busy true → provider fetches → busy false ) begins at the data-provider level, which can only start once the thread is free again ~1 s later. So the long, spinner-less pause is dominated by this click-level validation block , upstream of the loading indicator’s own busy window .. the load (and with it the spinner) simply doesn’t get to start until the main thread is unblocked.
Hint: this was not a TreeEngine bug and not the data loader. It was application-side validation running on the wrong path. An artificial delay(…) in the loader appeared to “fix” it, but only by accident (it pushed setData into a later task) .. the real culprit was the blocking validation on the click.
How to find it if you see this
- Confirm with Redux DevTools that busy: true reaches the store and that time-lapsing states renders the spinner. If it does, the pipeline is fine and it’s a timing/blocking problem.
- Take a browser performance profile of one expansion click. Check whether the long task is inside your loader, inside rendering, or (as here) inside the synchronous dispatch/selector chain of the click itself, before the load starts.
- Check whether any selector or type-guard does non-trivial work (schema validation, deep scans) that re-runs on every store notification.
Possible solutions
- Move heavy validation off the hot selector path. Validate once when data is ingested (i.e. when it’s resolved and stored), not on every selector run. This was our fix, and it removed the block entirely.
- Use a cheap check in the selector (e.g. a descriptor/discriminant-based guard) that still guarantees the correct return type, instead of parsing the full dataset.
- Memoize the guard by data reference so identical data isn’t re-validated.
- If your case really is a slow-but-legitimately-async load (theory 2/3), consider virtualization (enableVirtualScroll + fixed rowHeight) and React.memo on rows to keep the expansion cheap enough to paint the busy frame.
Once the per-dispatch main-thread block was gone, the expansion became responsive and the row ProgressIndicator showed as expected again.