Skip to content

fix(apollo-react): compute node height from handle count to stop flicker [MST-11677]#867

Open
KodudulaAshishUiPath wants to merge 1 commit into
mainfrom
fix/MST-11677-basenode-height-flicker
Open

fix(apollo-react): compute node height from handle count to stop flicker [MST-11677]#867
KodudulaAshishUiPath wants to merge 1 commit into
mainfrom
fix/MST-11677-basenode-height-flicker

Conversation

@KodudulaAshishUiPath

@KodudulaAshishUiPath KodudulaAshishUiPath commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes the Switch-node height flicker (MST-11677): deleting a case from an unconnected Switch made it oscillate between the 2-handle (96px) and 3-handle (128px) sizes. Root cause was a feedback loop where BaseNode derived its height from the measured height prop and wrote it back via updateNode inside a requestAnimationFrame.

Changes

  • computedHeight is now a pure function of visible handle count + intrinsic footer/default height (getIntrinsicHeight). It never reads the measured height prop, so the value written back can't feed into its own input — the loop is structurally gone.
  • A single, synchronous useEffect writes computedHeight to node.height (no rAF), only when it differs from the explicit getNode(id)?.height, then calls updateNodeInternals. Comparing against the explicit height (not the measured prop) means a lagging ResizeObserver measurement can't retrigger the write. Convergent and idempotent.
  • Keeping node.height authoritative fixes the downstream model: measured height, edge anchoring, fit-view, selection bounds, and handle spacing all agree with the rendered body (fixes handles overflowing on high-fan-out nodes).
  • animatedViewportManager now reads node.measured?.height ?? node.height ?? …, matching the package's dominant convention (previously node.height || 100, which went stale when height wasn't written).
  • Removed the old baseHeightRef/syncedHeightRef bookkeeping and the dead getContainerHeight height param.

Flow

flowchart TD
    A[handleConfigurations / footer] --> B[computedHeight - pure]
    B --> C{getNode.height != computedHeight?}
    C -- yes --> D[updateNode height + updateNodeInternals]
    C -- no --> E[no-op: converged]
    D --> F[node.height authoritative]
    F --> G[measured height / edges / handle spacing agree]
Loading

Testing

  • pnpm lint (Biome) passes
  • pnpm typecheck passes
  • Tests pass — rewritten height suite asserts the write-once/converge behavior, the 128→96 deflation settles with no re-inflation, and no write when already correct (full apollo-react suite green, 1885)
  • pnpm test:visual — runs in CI
  • No as any / type suppressions added

Copilot AI review requested due to automatic review settings June 29, 2026 18:39
@github-actions

github-actions Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (PT)
apollo-design 🟢 Ready Preview, Logs Jul 03, 2026, 12:53:49 AM
apollo-docs 🟢 Ready Preview, Logs Jul 03, 2026, 12:53:49 AM
apollo-landing 🟢 Ready Preview, Logs Jul 03, 2026, 12:53:49 AM
apollo-vertex 🟢 Ready Preview, Logs Jul 03, 2026, 12:53:49 AM

@github-actions

Copy link
Copy Markdown
Contributor

Dependency License Review

  • 1945 package(s) scanned
  • ✅ No license issues found
  • ⚠️ 2 package(s) excluded (see details below)
License distribution
License Packages
MIT 1715
ISC 89
Apache-2.0 55
BSD-3-Clause 27
BSD-2-Clause 23
BlueOak-1.0.0 8
MPL-2.0 4
MIT-0 3
CC0-1.0 3
MIT OR Apache-2.0 2
(MIT OR Apache-2.0) 2
Unlicense 2
LGPL-3.0-or-later 1
Python-2.0 1
CC-BY-4.0 1
(MPL-2.0 OR Apache-2.0) 1
Unknown 1
Artistic-2.0 1
(WTFPL OR MIT) 1
(BSD-2-Clause OR MIT OR Apache-2.0) 1
CC-BY-3.0 1
0BSD 1
(MIT OR CC0-1.0) 1
MIT AND ISC 1
Excluded packages
Package Version License Reason
@img/sharp-libvips-linux-x64 1.2.4 LGPL-3.0-or-later LGPL pre-built binary, not linked
khroma 2.1.0 Unknown MIT per GitHub repo, missing license field in package.json

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a React Flow node-height oscillation in apollo-react canvas BaseNode caused by a deferred requestAnimationFrame height sync briefly stamping a “synced” height before the write actually committed, allowing a stale in-flight height to be treated as an external resize.

Changes:

  • Move syncedHeightRef stamping into the rAF callback so it only records heights that were actually committed via updateNode.
  • Tighten comments around syncedHeightRef / baseHeightRef semantics to reflect commit-time stamping and the external-vs-internal height distinction.
  • Add a regression test that keeps rAF deferred and reproduces the stale interleaved render scenario, asserting the node deflates and remains stable.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
packages/apollo-react/src/canvas/components/BaseNode/BaseNode.tsx Stamps syncedHeightRef at rAF commit time (paired with updateNode) to prevent stale in-flight heights from poisoning baseHeightRef.
packages/apollo-react/src/canvas/components/BaseNode/BaseNode.test.tsx Adds a deferred-rAF regression test to reproduce and prevent the 96px ↔ 128px flicker loop.

@github-actions

github-actions Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

📊 Coverage + size by package

Per-package coverage and bundle size on this PR. New-line coverage = of the source lines this PR adds or changes, the % hit by tests.

Package Coverage New-line coverage Packed (gzip) Unpacked vs main
@uipath/apollo-core 9.0% 43.82 MB 57.31 MB ±0
@uipath/apollo-react 34.4% 100.0% (10/10) 7.27 MB 27.60 MB −356 B
@uipath/apollo-wind 40.1% 392.6 KB 2.55 MB +18 B
@uipath/ap-chat 85.8% 43.41 MB 55.85 MB ±0

"Coverage" is each package's own coverage.include scope (e.g. apollo-core instruments only scripts/). "Packed"/"Unpacked" come from npm pack --dry-run and only cover built packages — "—" means not measured this run (package not affected / not built). "vs main" is the packed (gzipped) delta against the last successful main build (the package-sizes artifact from the Release workflow); "—" there means no main baseline was available this run. The baseline is main's latest build, not this PR's exact merge-base, so it includes any drift since the branch diverged. Packages with no vitest config are omitted.

Comment thread packages/apollo-react/src/canvas/components/BaseNode/BaseNode.tsx Outdated
@KodudulaAshishUiPath KodudulaAshishUiPath force-pushed the fix/MST-11677-basenode-height-flicker branch from 04c3ca9 to 7bae0a7 Compare July 1, 2026 13:30
@github-actions github-actions Bot added size:L 100-499 changed lines. and removed size:M 30-99 changed lines. labels Jul 1, 2026
@KodudulaAshishUiPath KodudulaAshishUiPath changed the title fix(apollo-react): stop node height flicker on handle count change [MST-11677] fix(apollo-react): make node height content-driven to stop handle flicker [MST-11677] Jul 1, 2026
const intrinsic = getContainerHeight(undefined, !!footerComponent, footerVariant);
const intrinsicFloor = typeof intrinsic === 'number' ? intrinsic : NODE_HEIGHT_DEFAULT;

return Math.max(intrinsicFloor, handleFloor);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this somehow affects the handle spacing so some scenarios are breaking:

  • Handles become offset from the grid
  • Handles grow out of node bounds

You can compare the existing storybook with preview storybook to see behavior differences.

Screen.Recording.2026-07-01.at.8.46.47.AM.mov

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Ben — addressed in ed95b26.

Root cause: handle spacing is computed from the nodeHeight prop, which is React Flow's measured height. When the height write-back was removed, that value was no longer tied to the handle-count floor — and because the stories/nodes set an explicit node.height (e.g. the Dynamic Handles story's height: 96), the measured height never grew to the floor. So the handles were distributed over the wrong height (off-grid, and overflowing the node bounds on high-fan-out nodes like the 3-input/7-output switch).

Fix: computedHeight (a pure function of handle count + footer) is now written back to node.height, so React Flow's measured height matches the rendered body and the grid math (calculateGridAlignedHandlePositions) gets the correct height. No feedback loop is reintroduced, because computedHeight never reads the measured height.

Could you re-check against the preview storybook once the new build is up?

Copilot AI review requested due to automatic review settings July 2, 2026 12:23
@KodudulaAshishUiPath KodudulaAshishUiPath force-pushed the fix/MST-11677-basenode-height-flicker branch from 7bae0a7 to ed95b26 Compare July 2, 2026 12:23
@KodudulaAshishUiPath KodudulaAshishUiPath changed the title fix(apollo-react): make node height content-driven to stop handle flicker [MST-11677] fix(apollo-react): compute node height from handle count to stop flicker [MST-11677] Jul 2, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

Comment on lines +276 to 283
// Write computedHeight to node.height and recalculate handle positions. Compare
// against node.height (not the measured `height` prop) so a lagging measurement
// can't retrigger the write.
// biome-ignore lint/correctness/useExhaustiveDependencies: handleConfigurations triggers handle recalculation.
useEffect(() => {
if (computedHeight !== undefined && computedHeight !== height) {
syncedHeightRef.current = computedHeight;
const frameId = requestAnimationFrame(() => {
updateNode(id, { height: computedHeight });
updateNodeInternals(id);
});

return () => {
cancelAnimationFrame(frameId);
};
if (getNode(id)?.height !== computedHeight) {
updateNode(id, { height: computedHeight });
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — the PR description was stale and is now updated. The write-back is intentional and required: React Flow's node model (measured height, edge anchoring, fit-view, selection bounds, and handle spacing) all read node.height, so we do need to keep it in sync.

The key change from the previous flickering version is that computedHeight is a pure function of handle count + footer and never reads the measured height prop — so writing it back cannot feed into its own input. That's what breaks the old feedback loop, not the removal of the write. The guard compares against the explicit node.height (via getNode), not the measured prop, so a lagging ResizeObserver measurement can't retrigger the write.

Comment on lines 319 to 357
@@ -378,7 +353,7 @@ const BaseNodeComponent = (props: NodeProps<Node<BaseNodeData>>) => {

return {
'--node-w': `${containerWidth}px`,
'--node-h': typeof containerHeight === 'number' ? `${containerHeight}px` : 'auto',
'--node-h': `${containerHeight}px`,
'--node-radius': nodeRadius,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, and the description is updated to match. Height is deliberately handle/footer-driven, not content-driven: computedHeight is written to node.height as the authoritative size, and BaseNodeContainer applies it as a fixed h-(--node-h). The earlier min-h-(--node-h) h-auto experiment was reverted for exactly the consistency reason you note — with an authoritative node.height, a fixed height keeps the body, the React Flow wrapper, and the measured height in agreement. Node content (icon/label, or fixed-height footer variants) is expected to fit within the computed height by construction.

@KodudulaAshishUiPath KodudulaAshishUiPath force-pushed the fix/MST-11677-basenode-height-flicker branch from ed95b26 to ad23359 Compare July 3, 2026 07:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pkg:apollo-react size:L 100-499 changed lines.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants