Redux: actions have inconsistent payloads and/or huge payloads, state has circular references, ReduxDevTools crashes
See original GitHub issueBug Report
Describe the Bug
Recently I’ve encountered the following problems in how Redux is used in the OHIF viewer and libraries:
-
Some of the Redux actions dispatched in the viewer have inconsistent payloads. That is, an action of a given type may have data layout completely different from another action of the same type. This is unsafe and violates the very basic principles of Redux: clients consuming these actions (whether OHIF or third party) cannot count on receiving a particular piece of data anymore. This leads to frustration and breakage.
-
Some of the actions have payloads that are way too big for any real-world use
-
Some actions result in Redux state having circular references in its object layout
-
Redux Dev Tools are slow and may crash unpredictably
What steps can we follow to reproduce the bug?
-
Launch a chromium-based browser and install latest version of Redux Dev Tools (2.17.0 as of today)
-
Open Chrome Dev Tools and switch to “Redux” tab
-
Open, for example, https://viewer.ohif.org/viewer/1.2.826.0.13854362241694438965858641723883466450351448
Patient Name: Dummy MRN: 716713622818558421526494572171041020891020924597 Study: 1.2.826.0.13854362241694438965858641723883466450351448
-
Click on first
VIEWPORT::SET
action in actions sidebar. Click “Action” tab. You will see that this action dispathes the following object:
{
type: 'VIEWPORT::SET',
viewportIndex: 0,
data: {
displaySetInstanceUid: '7d860e86-cb2f-2758-4a55-a8815e93e311',
studyInstanceUid: '1.2.826.0.13854362241694438965858641723883466450351448',
currentImageIdIndex: 0,
sopInstanceUid: '1.2.826.0.13567014206729755965107989533871182029618726'
}
}
Click on next VIEWPORT::SET
action in actions sidebar. You will see that this action has completely different data:
{
type: 'VIEWPORT::SET',
viewportIndex: 0,
data: {
plugin: "cornerstone",
dom: "<120k lines of json which make my computer hang for a minute>"
}
}
- Further, scroll slices in the viewport, try to drag the “SEG” series onto the viewport.
RESET_LABELLING_AND_CONTEXT
and moreVIEWPORT:SET
actions will be dispatched. At some point Redux Dev Tools will stop responding to user interaction and the popup “Redux DevTools has crashed” will appear:
- If one is quick enough, before dev tools crash, they may be able to catch the following pink elephants in state snapshots on “State” tab (note
'[CIRCULAR]'
):
{
...
viewports: '[CIRCULAR]',
...
}
{
...
viewports: {
activeViewportIndex: 0,
layout: {
viewports: [
{
height: '100%',
width: '100%',
plugin: 'cornerstone'
}
]
},
viewportSpecificData: {
'0': {
plugin: 'cornerstone',
dom: '[CIRCULAR]',
displaySetInstanceUid: '70ade2ad-d07e-c252-49c8-0d212aacba16',
studyInstanceUid: '1.2.826.0.13854362241694438965858641723883466450351448',
currentImageIdIndex: 0,
sopInstanceUid: '1.2.826.0.11521735486436309604024223829282084628708383',
seriesDate: '20190129',
seriesTime: '153943',
seriesInstanceUid: '1.2.826.0.53608788865337544170015089925954063493496832',
seriesNumber: 1001,
seriesDescription: 'White matter hyperintensity segmentation',
numImageFrames: 48,
modality: 'SEG',
isMultiFrame: true,
instanceNumber: 1,
sopClassUids: [
'1.2.840.10008.5.1.4.1.1.66.4'
],
isClip: true
}
}
},
...
}
Possible resolution strategies
- Consider adopting a stable action layout. Action creators shoud be made more type safe. It is not enough to just pass
data
to action creator and to insertdata
into the action object as is. Every field ofdata
should be specified separately to ensure that there is no divergence indata
type. Example:
function setViewportData(data) {
return {
type: 'VIEWPORT:SET',
// data can be anything
data
};
}
dispatch(setViewportData({ dom: myEntireDom }) // uh-oh! Where is my studyId?
function setViewportData(studyId, seriesId) {
return {
type: 'VIEWPORT:SET',
// data can only be an object containing studyId, seriesId. both can be anything
data: {
studyId,
seriesId,
}
};
}
dispatch(setViewportData({ studyId: '1.2.3', seriesId: undefined })) // better, but still
-
Consider adopting Flux Standard Action pattern
-
Consider switching relevant parts of the application and libraries to Typescript (see e.g.
@babel/preset-typescript
). Introducing types and type-safe actions, e.g. viatypescript-fsa
, will be a major improvement in terms of usability of these actions and of 3rdparty-developer experience. Example:
interface SetViewportDataParams {
studyId: string;
seriesId: string;
}
function setViewportData({ studyId, seriesId }: SetViewportDataParams) {
return {
type: 'VIEWPORT:SET',
// data can only be an object containing studyId and seriesId, both are strings
data: {
studyId,
seriesId,
}
};
}
dispatch(setViewportData({ dom: myEntireDom })) // compiler error
dispatch(setViewportData({ studyId: '1.2.3', seriesId: undefined })) // compiler error
dispatch(setViewportData({ studyId: '0.1.23', seriesId: '4.5.678' })) // okay
-
Consider using Redux Dev Tools more during development process in order to verify actions payloads and state changes. Developers typically expect Redux Dev Tools to be functioning on any project. It is frustrating when they are not: “What do I do now? console.log everything?”
-
Consider adding more unit tests for component logic
-
Should DOM objects be a part of the action payload? I think this particular
dom
entry is way to big to be useful -
Consider making eslint config more strict. There are eslint rules that may help to catch bugs in redux
Issue Analytics
- State:
- Created 4 years ago
- Reactions:2
- Comments:7 (6 by maintainers)
Top GitHub Comments
I believe we have very similar goals then ^_^
There is an in-progress PR here that adds some of that functionality to
react-cornerstone-viewport
, or at least makes it a bit cleaner: https://github.com/cornerstonejs/react-cornerstone-viewport/pull/41Where you can now set
imageIdIndex
for the component, and it willscrollToIndex
. The API changes we made there are a large part of why I am now touching theVIEWPORT
actions as I integrate the updates.I’m torn on adding more data to Redux. It’s a bit odd to add everything we need for every possible viewport implementation, overlay, etc. If we have key bits of information that should cause the view to change, like a studyId, then we should include that – but then that studyId can be used to query a service for other data that doesn’t need to be reactive.
I’m all ears here, as I’m new-ish to React development. If you have any specific problems that are not yet solved, or suggestions around a cleaner handshake, we could set up a quick call?
@ivan-aksamentov, thank you for the detailed write-up! Internally, we’re aware that entirely too much data is being jammed into Redux. We’re actively monitoring new PRs to make sure we don’t continue this creep, and we’re working to simplify this portion of the code base.
A large part of the problem is that the bulk of these changes were made as part of a massive refactor to use React. It’s common for new React developers to want to make everything reactive. Now that we’re here, our appetite for large refactors has waned, so we’re trying to clean these things up as we touch relevant portions of the code base.
Viewports in particular are hairy because they’re an “extension point”, with each exposed viewport module potentially requiring different data in order to function. We’re working to more clearly define the responsibilities of
core
,viewer
andviewport extensions
in this handshake. Currently, I believe we’re leaning toward information like:ids
,layout
, etc. continuing to exist in Redux, whilecore
would expose the remaining data (and DICOMWeb / API calls, cached image data, etc) through services, some of which we might expose to extensions.The path’s not super clear yet, but please feel free to weigh in or draft reports of other problems/solutions as you encounter them 🙏