fix(microscopy): Prevent measurement clearing on viewport resize (#5796)

Wrap DicomMicroscopyViewport with React.memo and custom areEqual
to prevent unnecessary re-renders that trigger clearAnnotations().

---------

Co-authored-by: Joe Boccanfuso <joe.boccanfuso@radicalimaging.com>
This commit is contained in:
Ghadeer Albattarni 2026-02-11 10:00:26 -05:00 committed by GitHub
parent 7cdc54c86d
commit baef707e46
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 299 additions and 222 deletions

View File

@ -7,7 +7,13 @@ import getDicomWebClient from './utils/dicomWebClient';
import dcmjs from 'dcmjs'; import dcmjs from 'dcmjs';
import { useSystem } from '@ohif/core'; import { useSystem } from '@ohif/core';
function DicomMicroscopyViewport({ // This component is wrapped with React.memo and uses a custom areEqual comparison
// function to prevent unnecessary re-renders when props are semantically identical
// (e.g. `displaySets` reference changes, but the content is the same).
// Note: If this component starts depending on additional props,
// update `areEqual` accordingly.
const DicomMicroscopyViewport = React.memo(
({
activeViewportId, activeViewportId,
setViewportActive, setViewportActive,
displaySets, displaySets,
@ -21,7 +27,7 @@ function DicomMicroscopyViewport({
viewportId: string; viewportId: string;
dataSource: any; dataSource: any;
resizeRef: any; resizeRef: any;
}) { }) => {
const { servicesManager, extensionManager } = useSystem(); const { servicesManager, extensionManager } = useSystem();
const [isLoaded, setIsLoaded] = useState(false); const [isLoaded, setIsLoaded] = useState(false);
const [viewer, setViewer] = useState(null); const [viewer, setViewer] = useState(null);
@ -181,24 +187,27 @@ function DicomMicroscopyViewport({
await loadViewer(smDisplaySet.others); await loadViewer(smDisplaySet.others);
if (displaySet.isOverlayDisplaySet && !displaySet.isLoaded && !displaySet.isLoading) { if (displaySet.isOverlayDisplaySet && !displaySet.isLoaded && !displaySet.isLoading) {
displaySet.load(smDisplaySet); const referencedDisplaySet = displaySet.getSourceDisplaySet();
displaySet.load(referencedDisplaySet);
} }
}, },
[dataSource, extensionManager, microscopyService, servicesManager, viewportId] [dataSource, extensionManager, microscopyService, servicesManager, viewportId]
); );
useEffect(() => { useEffect(() => {
if (!viewer) {
const displaySet = displaySets[0]; const displaySet = displaySets[0];
installOpenLayersRenderer(container.current, displaySet).then(() => { installOpenLayersRenderer(container.current, displaySet).then(() => {
setIsLoaded(true); setIsLoaded(true);
}); });
}
return () => { return () => {
if (viewer) { if (viewer) {
microscopyService.removeViewer(viewer); microscopyService.removeViewer(viewer);
} }
}; };
}, []); }, [viewer]);
useEffect(() => { useEffect(() => {
const displaySet = displaySets[0]; const displaySet = displaySets[0];
@ -253,6 +262,69 @@ function DicomMicroscopyViewport({
{isLoaded ? null : <LoadingIndicatorProgress className={'h-full w-full bg-black'} />} {isLoaded ? null : <LoadingIndicatorProgress className={'h-full w-full bg-black'} />}
</div> </div>
); );
},
areEqual
);
// Check if the props are the same
function areEqual(prevProps, nextProps) {
if (prevProps.setViewportActive !== nextProps.setViewportActive) {
return false;
} }
if (prevProps.resizeRef !== nextProps.resizeRef) {
return false;
}
if (prevProps.viewportId !== nextProps.viewportId) {
return false;
}
if (prevProps.activeViewportId !== nextProps.activeViewportId) {
return false;
}
if (prevProps.dataSource !== nextProps.dataSource) {
return false;
}
const prevDisplaySets = prevProps.displaySets;
const nextDisplaySets = nextProps.displaySets;
if (prevDisplaySets.length !== nextDisplaySets.length) {
return false;
}
// Check if the displaySets are the same
for (let i = 0; i < prevDisplaySets.length; i++) {
const prevDisplaySet = prevDisplaySets[i];
const foundDisplaySet = nextDisplaySets.find(
nextDisplaySet =>
nextDisplaySet.displaySetInstanceUID === prevDisplaySet.displaySetInstanceUID
);
if (!foundDisplaySet) {
return false;
}
// Check if the displaySet's images are the same
if (foundDisplaySet.images?.length !== prevDisplaySet.images?.length) {
return false;
}
// Check if the displaySet's imageIds are the same
if (foundDisplaySet.images?.length) {
for (let j = 0; j < foundDisplaySet.images.length; j++) {
if (foundDisplaySet.images[j].imageId !== prevDisplaySet.images[j].imageId) {
return false;
}
}
}
}
return true;
}
DicomMicroscopyViewport.displayName = 'DicomMicroscopyViewport';
export default DicomMicroscopyViewport; export default DicomMicroscopyViewport;

View File

@ -1,5 +1,5 @@
import { id } from './id'; import { id } from './id';
import React, { Suspense, useMemo } from 'react'; import React, { Suspense, useCallback, useMemo } from 'react';
import getPanelModule from './getPanelModule'; import getPanelModule from './getPanelModule';
import getCommandsModule from './getCommandsModule'; import getCommandsModule from './getCommandsModule';
import getCustomizationModule from './getCustomizationModule'; import getCustomizationModule from './getCustomizationModule';
@ -81,13 +81,18 @@ const extension: Types.Extensions.Extension = {
handleWidth: true, handleWidth: true,
}); });
const setViewportActive = useCallback(
(viewportId: string) => {
viewportGridService.setActiveViewportId(viewportId);
},
[viewportGridService]
);
return ( return (
<MicroscopyViewport <MicroscopyViewport
key={displaySetsKey} key={displaySetsKey}
activeViewportId={activeViewportId} activeViewportId={activeViewportId}
setViewportActive={(viewportId: string) => { setViewportActive={setViewportActive}
viewportGridService.setActiveViewportId(viewportId);
}}
viewportData={viewportOptions} viewportData={viewportOptions}
resizeRef={resizeRef} resizeRef={resizeRef}
{...props} {...props}