Skip to content

Commit 213d788

Browse files
authored
Fixes issues with redux. (#315)
* Fixes issues with redux. We need to make this package's redux the same version as the apps main. We also need to make sure we don't update state inside a reducer and instead queue a microtask. Otherwise the app crashes when selecting datasets. * Removes optimistic use of state.
1 parent 2685dc0 commit 213d788

4 files changed

Lines changed: 92 additions & 34 deletions

File tree

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@code-dot-org/ml-playground",
3-
"version": "0.0.50",
3+
"version": "0.0.51",
44
"private": false,
55
"repository": {
66
"type": "git",
@@ -81,7 +81,7 @@
8181
"react": "^18.3.1",
8282
"react-dom": "^18.3.1",
8383
"react-papaparse": "^3.8.0",
84-
"react-redux": "^9.2.0",
84+
"react-redux": "^8.1.3",
8585
"redux": "^4.0.5",
8686
"style-loader": "^4.0.0",
8787
"ts-loader": "^9.5.7",

src/redux.ts

Lines changed: 47 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -416,10 +416,20 @@ export default function rootReducer(
416416
}
417417
if (action.type === SET_IMPORTED_DATA) {
418418
if (state.currentPanel === 'selectDataset') {
419-
state.instructionsKeyCallback!(
420-
action.userUploadedData ? 'uploadedDataset' : 'selectedDataset',
421-
null,
422-
);
419+
// Reducer must stay pure: the consumer-supplied callback dispatches
420+
// into its own redux store, which would interleave React commits
421+
// (and a getState cascade) into this dispatch and trip the
422+
// "getState() while reducer is executing" guard. Defer to a
423+
// microtask so the dispatch fully unwinds before the callback fires.
424+
if (state.instructionsKeyCallback) {
425+
const callback = state.instructionsKeyCallback;
426+
queueMicrotask(() =>
427+
callback(
428+
action.userUploadedData ? 'uploadedDataset' : 'selectedDataset',
429+
null,
430+
),
431+
);
432+
}
423433
}
424434

425435
return {
@@ -601,7 +611,13 @@ export default function rootReducer(
601611
state.viewedPanels.push(action.currentPanel);
602612
showedOverlay = true;
603613
}
604-
state.instructionsKeyCallback(action.currentPanel, options);
614+
// Deferred to a microtask — see the comment on the SET_IMPORTED_DATA
615+
// branch above for why the reducer must not synchronously fire a
616+
// consumer callback that dispatches into another store.
617+
const callback = state.instructionsKeyCallback;
618+
const callbackAction = action.currentPanel;
619+
const callbackOptions = options;
620+
queueMicrotask(() => callback(callbackAction, callbackOptions));
605621
}
606622

607623
if (action.currentPanel === 'dataDisplayLabel') {
@@ -670,24 +686,32 @@ export default function rootReducer(
670686
} else if (state.currentColumn === action.currentColumn) {
671687
// If column is selected, then deselect.
672688
if (state.currentPanel === 'dataDisplayFeatures') {
673-
state.instructionsKeyCallback!('dataDisplayFeatures', null);
689+
// Deferred — see SET_IMPORTED_DATA comment.
690+
if (state.instructionsKeyCallback) {
691+
const callback = state.instructionsKeyCallback;
692+
queueMicrotask(() => callback('dataDisplayFeatures', null));
693+
}
674694
}
675695
return {
676696
...state,
677697
currentColumn: undefined,
678698
};
679699
} else {
680700
if (state.currentPanel === 'dataDisplayFeatures') {
681-
if (
682-
state.columnsByDataType[action.currentColumn] ===
683-
ColumnTypes.NUMERICAL
684-
) {
685-
state.instructionsKeyCallback!('selectedFeatureNumerical', null);
686-
} else if (
687-
state.columnsByDataType[action.currentColumn] ===
688-
ColumnTypes.CATEGORICAL
689-
) {
690-
state.instructionsKeyCallback!('selectedFeatureCategorical', null);
701+
// Deferred — see SET_IMPORTED_DATA comment.
702+
if (state.instructionsKeyCallback) {
703+
const callback = state.instructionsKeyCallback;
704+
if (
705+
state.columnsByDataType[action.currentColumn] ===
706+
ColumnTypes.NUMERICAL
707+
) {
708+
queueMicrotask(() => callback('selectedFeatureNumerical', null));
709+
} else if (
710+
state.columnsByDataType[action.currentColumn] ===
711+
ColumnTypes.CATEGORICAL
712+
) {
713+
queueMicrotask(() => callback('selectedFeatureCategorical', null));
714+
}
691715
}
692716
}
693717

@@ -731,10 +755,13 @@ export default function rootReducer(
731755
};
732756
}
733757
if (action.type === SET_SHOW_RESULTS_DETAILS) {
734-
state.instructionsKeyCallback!(
735-
action.show ? 'resultsDetails' : 'results',
736-
null,
737-
);
758+
// Deferred — see SET_IMPORTED_DATA comment.
759+
if (state.instructionsKeyCallback) {
760+
const callback = state.instructionsKeyCallback;
761+
queueMicrotask(() =>
762+
callback(action.show ? 'resultsDetails' : 'results', null),
763+
);
764+
}
738765
return {
739766
...state,
740767
showResultsDetails: action.show,

webpack.config.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,14 @@ const externalConfig = {
8181
react: 'react',
8282
'react-dom': 'react-dom',
8383
'react/jsx-runtime': 'react/jsx-runtime',
84+
// Externalize redux + react-redux so the consumer's instances are
85+
// used at runtime. Bundling our own copies creates two react-redux
86+
// instances on the page (one ours, one the consumer's) sharing the
87+
// same React reconciler — their subscription notification chains
88+
// interleave and trip redux's "getState() while reducer is
89+
// executing" guard. One instance per page eliminates the race.
90+
redux: 'redux',
91+
'react-redux': 'react-redux',
8492
},
8593
};
8694

yarn.lock

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -993,6 +993,11 @@
993993
"@babel/plugin-transform-modules-commonjs" "^7.27.1"
994994
"@babel/plugin-transform-typescript" "^7.28.5"
995995

996+
"@babel/runtime@^7.12.1":
997+
version "7.29.7"
998+
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.29.7.tgz#12022450c45a4da6d8d8287b18a4ff2ddb23f768"
999+
integrity sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==
1000+
9961001
"@babel/template@^7.28.6", "@babel/template@^7.3.3":
9971002
version "7.28.6"
9981003
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.28.6.tgz#0e7e56ecedb78aeef66ce7972b082fce76a23e57"
@@ -1821,6 +1826,13 @@
18211826
dependencies:
18221827
"@types/node" "*"
18231828

1829+
"@types/hoist-non-react-statics@^3.3.1":
1830+
version "3.3.7"
1831+
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz#306e3a3a73828522efa1341159da4846e7573a6c"
1832+
integrity sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==
1833+
dependencies:
1834+
hoist-non-react-statics "^3.3.0"
1835+
18241836
"@types/http-errors@*":
18251837
version "2.0.5"
18261838
resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.5.tgz#5b749ab2b16ba113423feb1a64a95dcd30398472"
@@ -1952,10 +1964,10 @@
19521964
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8"
19531965
integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==
19541966

1955-
"@types/use-sync-external-store@^0.0.6":
1956-
version "0.0.6"
1957-
resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz#60be8d21baab8c305132eb9cb912ed497852aadc"
1958-
integrity sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==
1967+
"@types/use-sync-external-store@^0.0.3":
1968+
version "0.0.3"
1969+
resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43"
1970+
integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==
19591971

19601972
"@types/ws@^8.5.10":
19611973
version "8.18.1"
@@ -4241,6 +4253,13 @@ hermes-parser@^0.25.1:
42414253
dependencies:
42424254
hermes-estree "0.25.1"
42434255

4256+
hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
4257+
version "3.3.2"
4258+
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
4259+
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
4260+
dependencies:
4261+
react-is "^16.7.0"
4262+
42444263
hoopy@^0.1.4:
42454264
version "0.1.4"
42464265
resolved "https://registry.yarnpkg.com/hoopy/-/hoopy-0.1.4.tgz#609207d661100033a9a9402ad3dea677381c1b1d"
@@ -6125,7 +6144,7 @@ react-dom@^18.3.1:
61256144
loose-envify "^1.1.0"
61266145
scheduler "^0.23.2"
61276146

6128-
react-is@^16.13.1, react-is@^16.8.1:
6147+
react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1:
61296148
version "16.13.1"
61306149
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
61316150
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@@ -6143,13 +6162,17 @@ react-papaparse@^3.8.0:
61436162
"@types/papaparse" "^5.0.4"
61446163
papaparse "^5.2.0"
61456164

6146-
react-redux@^9.2.0:
6147-
version "9.3.0"
6148-
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-9.3.0.tgz#a30113bb6d95c0a715d54dda4308d450fca6ce09"
6149-
integrity sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==
6165+
react-redux@^8.1.3:
6166+
version "8.1.3"
6167+
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-8.1.3.tgz#4fdc0462d0acb59af29a13c27ffef6f49ab4df46"
6168+
integrity sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw==
61506169
dependencies:
6151-
"@types/use-sync-external-store" "^0.0.6"
6152-
use-sync-external-store "^1.4.0"
6170+
"@babel/runtime" "^7.12.1"
6171+
"@types/hoist-non-react-statics" "^3.3.1"
6172+
"@types/use-sync-external-store" "^0.0.3"
6173+
hoist-non-react-statics "^3.3.2"
6174+
react-is "^18.0.0"
6175+
use-sync-external-store "^1.0.0"
61536176

61546177
react@^18.3.1:
61556178
version "18.3.1"
@@ -7231,7 +7254,7 @@ uri-js@^4.2.2:
72317254
dependencies:
72327255
punycode "^2.1.0"
72337256

7234-
use-sync-external-store@^1.4.0:
7257+
use-sync-external-store@^1.0.0:
72357258
version "1.6.0"
72367259
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz#b174bfa65cb2b526732d9f2ac0a408027876f32d"
72377260
integrity sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==

0 commit comments

Comments
 (0)