Fix ExportImageDialog infinite re-render loop (#98)

* fix: eliminate re-render loop causing export failures

- Remove onModelUpdated callback that triggered infinite re-render cycles
- Replace debounce hack with controlled useEffect-based export timing
- Ensure DOM stability before export execution to prevent null containerRef
- Add isExporting guard to prevent concurrent export operations

Resolves issue where image export functionality was completely broken due to
infinite re-rendering caused by onModelUpdated callback.

Fixes #84

Huge thanks are attributed to @jibbex for resolving this issue!
This commit is contained in:
Manfred Michaelis
2025-08-21 10:37:47 +02:00
committed by GitHub
parent 627fe2e690
commit c626261e3e

View File

@@ -40,7 +40,7 @@ interface Props {
export const ExportImageDialog = ({ onClose, quality = 1.5 }: Props) => {
const containerRef = useRef<HTMLDivElement>();
const debounceRef = useRef<NodeJS.Timeout>();
const isExporting = useRef<boolean>(false);
const currentView = useUiStateStore((state) => {
return state.view;
});
@@ -65,33 +65,24 @@ export const ExportImageDialog = ({ onClose, quality = 1.5 }: Props) => {
});
}, [uiStateActions]);
const exportImage = useCallback(async () => {
if (!containerRef.current) return;
const exportImage = useCallback(() => {
if (!containerRef.current || isExporting.current) {
return;
}
clearTimeout(debounceRef.current);
debounceRef.current = setTimeout(() => {
exportAsImage(containerRef.current as HTMLDivElement)
.then((data) => {
return setImageData(data);
})
.catch((err) => {
console.log(err);
setExportError(true);
});
}, 2000);
isExporting.current = true;
exportAsImage(containerRef.current as HTMLDivElement)
.then((data) => {
setImageData(data);
isExporting.current = false;
})
.catch((err) => {
console.error(err);
setExportError(true);
isExporting.current = false;
});
}, []);
const downloadFile = useCallback(() => {
if (!imageData) return;
const data = base64ToBlob(
imageData.replace('data:image/png;base64,', ''),
'image/png;charset=utf-8'
);
downloadFileUtil(data, generateGenericFilename('png'));
}, [imageData]);
const [showGrid, setShowGrid] = useState(false);
const handleShowGridChange = (checked: boolean) => {
setShowGrid(checked);
@@ -104,10 +95,37 @@ export const ExportImageDialog = ({ onClose, quality = 1.5 }: Props) => {
setBackgroundColor(color);
};
// Reset image data when options change and trigger export
useEffect(() => {
setImageData(undefined);
setExportError(false);
isExporting.current = false;
const timer = setTimeout(() => {
exportImage();
}, 100);
return () => clearTimeout(timer);
}, [showGrid, backgroundColor]);
useEffect(() => {
const timer = setTimeout(() => {
exportImage();
}, 100);
return () => clearTimeout(timer);
}, []);
const downloadFile = useCallback(() => {
if (!imageData) return;
const data = base64ToBlob(
imageData.replace('data:image/png;base64,', ''),
'image/png;charset=utf-8'
);
downloadFileUtil(data, generateGenericFilename('png'));
}, [imageData]);
return (
<Dialog open onClose={onClose}>
<DialogTitle>Export as image</DialogTitle>
@@ -146,7 +164,6 @@ export const ExportImageDialog = ({ onClose, quality = 1.5 }: Props) => {
>
<Isoflow
editorMode="NON_INTERACTIVE"
onModelUpdated={exportImage}
initialData={{
...model,
fitToView: true,
@@ -235,4 +252,4 @@ export const ExportImageDialog = ({ onClose, quality = 1.5 }: Props) => {
</DialogContent>
</Dialog>
);
};
};