The Problem
When users clicked "Export as image," the entire canvas would visibly jump around, center itself, and snap back. This happened because the export function manipulated the visible canvas Issue #2750:
// Old approach - caused flicker
const graph = controller.getGraph();
graph.reset(); // User sees this happening!
graph.fit(80); // Canvas jumps around
graph.layout(); // Everything moves
await exportToPng(surface);
Users could see all these transformations happening in real-time before the export. Not a great experience.
My First Solution (That Got Rejected)
I submitted my Pull Request #2766 with a DOM cloning approach:
// Clone the visible canvas
const clone = visibleContainer.cloneNode(true) as HTMLElement;
clone.style.position = 'fixed';
clone.style.top = '-99999px';
// Transform and export the clone
await toPng(clone);
The flicker was gone! But the maintainer @lordrip explained this wasn't the right architectural approach. He wanted me to create a separate React component with its own controller that renders off-screen.
I was confused. Why rebuild everything when cloning worked?
Understanding the Real Solution
After reading Canvas.tsx, I understood the pattern. The canvas uses PatternFly's React Topology library with a controller:
const controller = useMemo(() => ControllerService.createController(), []);
The insight: create a second controller that renders invisibly. Transform that one, not the visible canvas!
Building the Hidden Canvas
I created HiddenCanvas.tsx that builds its own model from entities (same way the main Canvas does):
export const HiddenCanvas: FunctionComponent = (props) => {
const hiddenController = useMemo(() =>
ControllerService.createController(),
[]
);
// Build model from entities
const nodes: CanvasNode[] = [];
const edges: CanvasEdge[] = [];
props.entities.forEach((entity) => {
const { nodes: childNodes, edges: childEdges } =
FlowService.getFlowDiagram(entity.id, entity.toVizNode());
nodes.push(...childNodes);
edges.push(...childEdges);
});
Problems I Hit
TypeScript Errors: I tried copying nodes from the existing controller using toModel(), but this broke parent-child relationships. The fix was building a fresh model from entities using FlowService.getFlowDiagram().
Blank Exports: The SVG had no width/height attributes. I had to set them manually:
const bbox = svg.getBBox();
const padding = 10;
svg.setAttribute('width', String(bbox.width + padding * 2));
svg.setAttribute('height', String(bbox.height + padding * 2));
MobX Warnings: Graph transformations needed to be wrapped in action():
action(() => {
hiddenGraph.reset();
hiddenGraph.fit(10);
hiddenGraph.layout();
})();
Timing Issues: I had to wait for the layout to complete before exporting. Instead of a blind timeout, I checked if nodes were actually positioned.
What I Learned
The maintainer was right. DOM cloning was a hack. Creating a proper component:
- Is more maintainable
- Follows React patterns
- Works better with the library's architecture
- Teaches you how the system actually works
Technical skills gained:
- React context providers and controllers
- MobX state management
- SVG manipulation
- Async timing with Promises
- TypeScript type system
Results and What I Actually Learned
The final implementation completely eliminates the flicker. The visible canvas stays in place while the export happens in the background using the hidden controller. Users can now export their diagrams without any visual disruption.
But looking back, I understand why the maintainer pushed for the proper solution. DOM cloning might work, but it doesn't teach you anything about how the system actually works. By building a proper React component with its own controller, I learned:
- How React's context and provider pattern works in complex applications
- Why separating concerns matters (visible canvas vs export canvas)
- How to build models from data sources instead of copying existing instances
- The importance of understanding the library's architecture before writing code
The biggest lesson was that in open-source, it's not just about making something work - it's about making it work the right way so other developers can understand and maintain it later. The maintainer could have accepted my quick fix, but he took the time to explain why a better approach existed. That's what made this a real learning experience instead of just a bug fix.
Top comments (0)