From d366bdeccd08e4e2a0bb13e4c1a1edd48d28b829 Mon Sep 17 00:00:00 2001 From: Victor Lin <13424970+victorlin@users.noreply.github.com> Date: Tue, 10 Oct 2023 14:24:41 -0700 Subject: [PATCH 01/11] Comment out unused logging I assume this is in place for easy uncommenting to enable the logging middleware. In this case, leave the import commented out as well so that ESLint/TypeScript rule exceptions aren't necessary. --- src/store/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/store/index.js b/src/store/index.js index 31b5fba63..332f2fb6a 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -2,7 +2,7 @@ import { createStore, applyMiddleware, compose } from "redux"; import thunk from "redux-thunk"; import { changeURLMiddleware } from "../middleware/changeURL"; import rootReducer from "../reducers"; -import { loggingMiddleware } from "../middleware/logActions"; // eslint-disable-line @typescript-eslint/no-unused-vars +// import { loggingMiddleware } from "../middleware/logActions"; import { keepScatterplotStateInSync } from "../middleware/scatterplot"; const configureStore = (initialState) => { From e607f9ae76461bcd2ace66ac6f3a940cd1a19782 Mon Sep 17 00:00:00 2001 From: Victor Lin <13424970+victorlin@users.noreply.github.com> Date: Tue, 10 Oct 2023 16:05:39 -0700 Subject: [PATCH 02/11] Simplify Redux store setup initialState (now commonly named preloadedState in modern Redux docs) was unused. This means there is no need for the intermediate configureStore function (whose name was misleading since that is the name of a newer Redux Toolkit function). --- src/index.js | 4 +--- src/store/index.js | 44 ++++++++++++++++++++++---------------------- 2 files changed, 23 insertions(+), 25 deletions(-) diff --git a/src/index.js b/src/index.js index 050bd95a4..d831a0c6c 100644 --- a/src/index.js +++ b/src/index.js @@ -9,7 +9,7 @@ import React from "react"; import ReactDOM from "react-dom"; import { Provider } from "react-redux"; /* A U S P I C E I M P O R T S */ -import configureStore from "./store"; +import store from "./store"; import { initialiseGoogleAnalyticsIfRequired } from "./util/googleAnalytics"; import Root from "./root"; /* I N T E R N A T I O N A L I Z A T I O N */ @@ -26,8 +26,6 @@ import "./css/select.css"; /* FONTS */ import 'typeface-lato'; -const store = configureStore(); - /* set up non-redux state storage for the animation - use this conservitavely! */ if (!window.NEXTSTRAIN) {window.NEXTSTRAIN = {};} diff --git a/src/store/index.js b/src/store/index.js index 332f2fb6a..5312e71f3 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -5,26 +5,26 @@ import rootReducer from "../reducers"; // import { loggingMiddleware } from "../middleware/logActions"; import { keepScatterplotStateInSync } from "../middleware/scatterplot"; -const configureStore = (initialState) => { - const middleware = [ - thunk, - keepScatterplotStateInSync, - changeURLMiddleware, - // loggingMiddleware - ]; - const composedEnhancers = compose( - applyMiddleware(...middleware), - window.__REDUX_DEVTOOLS_EXTENSION__ ? window.__REDUX_DEVTOOLS_EXTENSION__() : (f) => f - ); - const store = createStore(rootReducer, initialState, composedEnhancers); - if (process.env.NODE_ENV !== 'production' && module.hot) { - // console.log("hot reducer reload"); - module.hot.accept('../reducers', () => { - const nextRootReducer = require('../reducers/index'); - store.replaceReducer(nextRootReducer); - }); - } - return store; -}; +const middleware = [ + thunk, + keepScatterplotStateInSync, + changeURLMiddleware, + // loggingMiddleware +]; -export default configureStore; +const composedEnhancers = compose( + applyMiddleware(...middleware), + window.__REDUX_DEVTOOLS_EXTENSION__ ? window.__REDUX_DEVTOOLS_EXTENSION__() : (f) => f +); + +const store = createStore(rootReducer, composedEnhancers); + +if (process.env.NODE_ENV !== 'production' && module.hot) { + // console.log("hot reducer reload"); + module.hot.accept('../reducers', () => { + const nextRootReducer = require('../reducers/index'); + store.replaceReducer(nextRootReducer); + }); +} + +export default store; From eb2c30f353483a8a544bf48cffe2677a6c7b3156 Mon Sep 17 00:00:00 2001 From: Victor Lin <13424970+victorlin@users.noreply.github.com> Date: Tue, 10 Oct 2023 14:57:31 -0700 Subject: [PATCH 03/11] Migrate store setup to Modern Redux MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow the guide¹. In short, configureStore automatically handles most of the store setup, including adding the thunk middleware and setting up the Redux DevTools Extension connection (now conditional on NODE_ENV). ¹ https://redux.js.org/usage/migrating-to-modern-redux#store-setup-with-configurestore --- package-lock.json | 85 ++++++++++++++++++++++++++++++++++++++-------- package.json | 3 +- src/store/index.js | 15 ++++---- 3 files changed, 78 insertions(+), 25 deletions(-) diff --git a/package-lock.json b/package-lock.json index ad3ba37c3..f7a0606af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@babel/preset-react": "^7.0.0", "@babel/preset-typescript": "^7.21.0", "@hot-loader/react-dom": "^16.13.0", + "@reduxjs/toolkit": "^1.9.7", "argparse": "^1.0.10", "babel-loader": "^8.0.4", "babel-plugin-lodash": "^3.3.4", @@ -78,8 +79,6 @@ "react-tooltip": "^4.2.10", "react-transition-group": "^1.2.1", "react-virtualized": "^9.22.3", - "redux": "^4.0.1", - "redux-thunk": "^2.3.0", "regenerator-runtime": "^0.13.5", "style-loader": "^0.13.2", "styled-components": "^4.0.3", @@ -2953,6 +2952,29 @@ "node": ">=12" } }, + "node_modules/@reduxjs/toolkit": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.7.tgz", + "integrity": "sha512-t7v8ZPxhhKgOKtU+uyJT13lu4vL7az5aFi4IdoDs/eS548edn2M8Ik9h8fxgvMjGoAUVFSt6ZC1P5cWmQ014QQ==", + "dependencies": { + "immer": "^9.0.21", + "redux": "^4.2.1", + "redux-thunk": "^2.4.2", + "reselect": "^4.1.8" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18", + "react-redux": "^7.2.1 || ^8.0.2" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@sideway/address": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", @@ -8130,6 +8152,15 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -12460,17 +12491,17 @@ } }, "node_modules/redux": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.1.2.tgz", - "integrity": "sha512-SH8PglcebESbd/shgf6mii6EIoRM0zrQyjcuQ+ojmfxjTtE0z9Y8pa62iA/OJ58qjP6j27uyW4kUF4jl/jd6sw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", "dependencies": { "@babel/runtime": "^7.9.2" } }, "node_modules/redux-thunk": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.0.tgz", - "integrity": "sha512-/y6ZKQNU/0u8Bm7ROLq9Pt/7lU93cT0IucYMrubo89ENjxPa7i8pqLKu6V4X7/TvYovQ6x01unTeyeZ9lgXiTA==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz", + "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==", "peerDependencies": { "redux": "^4" } @@ -12594,6 +12625,11 @@ "resolved": "https://registry.npmjs.org/require-package-name/-/require-package-name-2.0.1.tgz", "integrity": "sha1-wR6XJ2tluOKSP3Xav1+y7ww4Qbk=" }, + "node_modules/reselect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", + "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==" + }, "node_modules/resolve": { "version": "1.22.2", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", @@ -16616,6 +16652,17 @@ } } }, + "@reduxjs/toolkit": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.7.tgz", + "integrity": "sha512-t7v8ZPxhhKgOKtU+uyJT13lu4vL7az5aFi4IdoDs/eS548edn2M8Ik9h8fxgvMjGoAUVFSt6ZC1P5cWmQ014QQ==", + "requires": { + "immer": "^9.0.21", + "redux": "^4.2.1", + "redux-thunk": "^2.4.2", + "reselect": "^4.1.8" + } + }, "@sideway/address": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", @@ -20571,6 +20618,11 @@ "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", "dev": true }, + "immer": { + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==" + }, "import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -23760,17 +23812,17 @@ } }, "redux": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.1.2.tgz", - "integrity": "sha512-SH8PglcebESbd/shgf6mii6EIoRM0zrQyjcuQ+ojmfxjTtE0z9Y8pa62iA/OJ58qjP6j27uyW4kUF4jl/jd6sw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", "requires": { "@babel/runtime": "^7.9.2" } }, "redux-thunk": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.0.tgz", - "integrity": "sha512-/y6ZKQNU/0u8Bm7ROLq9Pt/7lU93cT0IucYMrubo89ENjxPa7i8pqLKu6V4X7/TvYovQ6x01unTeyeZ9lgXiTA==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz", + "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==", "requires": {} }, "regenerate": { @@ -23870,6 +23922,11 @@ "resolved": "https://registry.npmjs.org/require-package-name/-/require-package-name-2.0.1.tgz", "integrity": "sha1-wR6XJ2tluOKSP3Xav1+y7ww4Qbk=" }, + "reselect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", + "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==" + }, "resolve": { "version": "1.22.2", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", diff --git a/package.json b/package.json index efcf9decb..f2c82ce1b 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "@babel/preset-react": "^7.0.0", "@babel/preset-typescript": "^7.21.0", "@hot-loader/react-dom": "^16.13.0", + "@reduxjs/toolkit": "^1.9.7", "argparse": "^1.0.10", "babel-loader": "^8.0.4", "babel-plugin-lodash": "^3.3.4", @@ -108,8 +109,6 @@ "react-tooltip": "^4.2.10", "react-transition-group": "^1.2.1", "react-virtualized": "^9.22.3", - "redux": "^4.0.1", - "redux-thunk": "^2.3.0", "regenerator-runtime": "^0.13.5", "style-loader": "^0.13.2", "styled-components": "^4.0.3", diff --git a/src/store/index.js b/src/store/index.js index 5312e71f3..c962249b2 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -1,23 +1,20 @@ -import { createStore, applyMiddleware, compose } from "redux"; -import thunk from "redux-thunk"; +import { configureStore } from '@reduxjs/toolkit'; import { changeURLMiddleware } from "../middleware/changeURL"; import rootReducer from "../reducers"; // import { loggingMiddleware } from "../middleware/logActions"; import { keepScatterplotStateInSync } from "../middleware/scatterplot"; const middleware = [ - thunk, keepScatterplotStateInSync, changeURLMiddleware, // loggingMiddleware ]; -const composedEnhancers = compose( - applyMiddleware(...middleware), - window.__REDUX_DEVTOOLS_EXTENSION__ ? window.__REDUX_DEVTOOLS_EXTENSION__() : (f) => f -); - -const store = createStore(rootReducer, composedEnhancers); +const store = configureStore({ + reducer: rootReducer, + middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(middleware), + devTools: process.env.NODE_ENV !== 'production', +}) if (process.env.NODE_ENV !== 'production' && module.hot) { // console.log("hot reducer reload"); From dcd83c0dcb10cabee1fab827f314d97176d4ae94 Mon Sep 17 00:00:00 2001 From: Victor Lin <13424970+victorlin@users.noreply.github.com> Date: Tue, 10 Oct 2023 15:18:49 -0700 Subject: [PATCH 04/11] Disable new default middleware From the migration guide: > [configureStore] automatically added more middleware to check for > common mistakes like accidentally mutating the state It turns out that these "common mistakes" are currently used in practice, and only noticeable now with console errors after the Redux migration. Disable the new checks for now. --- src/store/index.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/store/index.js b/src/store/index.js index c962249b2..c1698840c 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -12,7 +12,14 @@ const middleware = [ const store = configureStore({ reducer: rootReducer, - middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(middleware), + middleware: (getDefaultMiddleware) => + getDefaultMiddleware({ + // TODO: Go through reducers and see why the state is not immutable nor serializable. + // These were not checked prior to the adoption of Redux Toolkit, and were not + // investigated to minimize conversion efforts. + immutableCheck: false, + serializableCheck: false + }).concat(middleware), devTools: process.env.NODE_ENV !== 'production', }) From 80ef4296824c9c9431b29704b3b7adfbeb2bd20f Mon Sep 17 00:00:00 2001 From: Victor Lin <13424970+victorlin@users.noreply.github.com> Date: Wed, 11 Oct 2023 17:04:31 -0700 Subject: [PATCH 05/11] tsconfig: Skip type checking all .d.ts files This was disabled in 80835fc1dcedf051d864a8541bf7a9d7451812c6, however there are issues with a few type declaration files in @reduxjs/toolkit that I don't want to bother tracking down if an earlier version can be used. It was originally disabled with an idea that this codebase might have some of its own declaration files, however I don't see that being necessary ever, or at least in the near future. --- tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/tsconfig.json b/tsconfig.json index a03dcf66c..e4c2ade4e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -44,6 +44,7 @@ Visit https://aka.ms/tsconfig.json for a detailed list of options. /* Completeness */ "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true, /* Skip type checking all .d.ts files. */ }, "include": [ "src/**/*" From 3fda4b0cbb5a7eabce43563f46bdf5759018b2ec Mon Sep 17 00:00:00 2001 From: Victor Lin <13424970+victorlin@users.noreply.github.com> Date: Tue, 10 Oct 2023 16:23:59 -0700 Subject: [PATCH 06/11] =?UTF-8?q?Rename=20src/store/index.js=20=E2=86=92?= =?UTF-8?q?=20src/store.js?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There aren't and shouldn't be any other files under src/store. This new naming is what's used in latest Redux documentation. --- src/{store/index.js => store.js} | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) rename src/{store/index.js => store.js} (70%) diff --git a/src/store/index.js b/src/store.js similarity index 70% rename from src/store/index.js rename to src/store.js index c1698840c..99b915c6c 100644 --- a/src/store/index.js +++ b/src/store.js @@ -1,8 +1,8 @@ import { configureStore } from '@reduxjs/toolkit'; -import { changeURLMiddleware } from "../middleware/changeURL"; -import rootReducer from "../reducers"; -// import { loggingMiddleware } from "../middleware/logActions"; -import { keepScatterplotStateInSync } from "../middleware/scatterplot"; +import { changeURLMiddleware } from "./middleware/changeURL"; +import rootReducer from "./reducers"; +// import { loggingMiddleware } from "./middleware/logActions"; +import { keepScatterplotStateInSync } from "./middleware/scatterplot"; const middleware = [ keepScatterplotStateInSync, @@ -25,8 +25,8 @@ const store = configureStore({ if (process.env.NODE_ENV !== 'production' && module.hot) { // console.log("hot reducer reload"); - module.hot.accept('../reducers', () => { - const nextRootReducer = require('../reducers/index'); + module.hot.accept('./reducers', () => { + const nextRootReducer = require('./reducers/index'); store.replaceReducer(nextRootReducer); }); } From 118ae19eaffb35390ef3106b4e8426de0846d546 Mon Sep 17 00:00:00 2001 From: Victor Lin <13424970+victorlin@users.noreply.github.com> Date: Tue, 10 Oct 2023 16:31:33 -0700 Subject: [PATCH 07/11] Convert store to TypeScript Export types for the store, inferred from the store setup process. These are unused for now, but will be useful for future TypeScript adoption of anything that accesses the store. --- package-lock.json | 13 +++++++++++++ package.json | 1 + src/{store.js => store.ts} | 4 ++++ tsconfig.json | 2 +- 4 files changed, 19 insertions(+), 1 deletion(-) rename src/{store.js => store.ts} (89%) diff --git a/package-lock.json b/package-lock.json index f7a0606af..edb9988ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -97,6 +97,7 @@ "devDependencies": { "@types/leaflet": "^1.9.3", "@types/node": "^18.15.11", + "@types/webpack-env": "^1.18.2", "@typescript-eslint/eslint-plugin": "^5.57.0", "@typescript-eslint/parser": "^5.57.0", "chai": "^4.1.2", @@ -3293,6 +3294,12 @@ "source-map": "^0.6.0" } }, + "node_modules/@types/webpack-env": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.18.2.tgz", + "integrity": "sha512-BFqcTHHTrrI8EBmIzNAmLPP3IqtEG9J1IPFWbPeS/F0/TGNmo0pI5svOa7JbMF9vSCXQCvJWT2gxLJNVuf9blw==", + "dev": true + }, "node_modules/@types/webpack-sources": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-3.2.0.tgz", @@ -16987,6 +16994,12 @@ } } }, + "@types/webpack-env": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.18.2.tgz", + "integrity": "sha512-BFqcTHHTrrI8EBmIzNAmLPP3IqtEG9J1IPFWbPeS/F0/TGNmo0pI5svOa7JbMF9vSCXQCvJWT2gxLJNVuf9blw==", + "dev": true + }, "@types/webpack-sources": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-3.2.0.tgz", diff --git a/package.json b/package.json index f2c82ce1b..ebaa77595 100644 --- a/package.json +++ b/package.json @@ -124,6 +124,7 @@ "devDependencies": { "@types/leaflet": "^1.9.3", "@types/node": "^18.15.11", + "@types/webpack-env": "^1.18.2", "@typescript-eslint/eslint-plugin": "^5.57.0", "@typescript-eslint/parser": "^5.57.0", "chai": "^4.1.2", diff --git a/src/store.js b/src/store.ts similarity index 89% rename from src/store.js rename to src/store.ts index 99b915c6c..8ea3bee76 100644 --- a/src/store.js +++ b/src/store.ts @@ -31,4 +31,8 @@ if (process.env.NODE_ENV !== 'production' && module.hot) { }); } +// Infer types from the store. +export type RootState = ReturnType +export type AppDispatch = typeof store.dispatch + export default store; diff --git a/tsconfig.json b/tsconfig.json index e4c2ade4e..37a8d7705 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,7 +16,7 @@ Visit https://aka.ms/tsconfig.json for a detailed list of options. /* Modules */ "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ - "types": ["node"], /* Specify type package names to be included without being referenced in a source file. */ + "types": ["node", "webpack-env"], /* Specify type package names to be included without being referenced in a source file. */ /* JavaScript Support */ "allowJs": true, /* Allow JavaScript files to be a part of your program. This allows TS files to import from JS files. */ From d1c3f292d5d842f389e5e5f53fea6ba2952882d0 Mon Sep 17 00:00:00 2001 From: Victor Lin <13424970+victorlin@users.noreply.github.com> Date: Tue, 12 Sep 2023 11:20:26 -0700 Subject: [PATCH 08/11] panel-toggles: Rename for TypeScript conversion --- src/components/controls/{panel-toggles.js => panel-toggles.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/components/controls/{panel-toggles.js => panel-toggles.tsx} (100%) diff --git a/src/components/controls/panel-toggles.js b/src/components/controls/panel-toggles.tsx similarity index 100% rename from src/components/controls/panel-toggles.js rename to src/components/controls/panel-toggles.tsx From 0b8bac0300f0936c0ae1fafeaa50a53b912147b1 Mon Sep 17 00:00:00 2001 From: Victor Lin <13424970+victorlin@users.noreply.github.com> Date: Tue, 10 Oct 2023 17:26:54 -0700 Subject: [PATCH 09/11] controls: Add types used in panel-toggles Previously, the controls state could not be implicitly typed. This starts things off for further adoption. --- src/reducers/{controls.js => controls.ts} | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) rename src/reducers/{controls.js => controls.ts} (95%) diff --git a/src/reducers/controls.js b/src/reducers/controls.ts similarity index 95% rename from src/reducers/controls.js rename to src/reducers/controls.ts index 0691f3cba..58b20293f 100644 --- a/src/reducers/controls.js +++ b/src/reducers/controls.ts @@ -12,11 +12,22 @@ import { calcBrowserDimensionsInitialState } from "./browserDimensions"; import { doesColorByHaveConfidence } from "../actions/recomputeReduxState"; import { hasMultipleGridPanels } from "../actions/panelDisplay"; +export interface ControlsState { + panelsAvailable: string[] + panelsToDisplay: string[] + showTreeToo: boolean + canTogglePanelLayout: boolean + + // This allows arbitrary prop names while TypeScript adoption is incomplete. + // TODO: add all other props explicitly and remove this. + [propName: string]: any; +} + /* defaultState is a fn so that we can re-create it at any time, e.g. if we want to revert things (e.g. on dataset change) */ export const getDefaultControlsState = () => { - const defaults = { + const defaults: Partial = { distanceMeasure: defaultDistanceMeasure, layout: defaultLayout, geoResolution: defaultGeoResolution, @@ -80,7 +91,7 @@ export const getDefaultControlsState = () => { panelsToDisplay: [], panelLayout: calcBrowserDimensionsInitialState().width > twoColumnBreakpoint ? "grid" : "full", tipLabelKey: defaults.tipLabelKey, - showTreeToo: undefined, + showTreeToo: false, showTangle: false, zoomMin: undefined, zoomMax: undefined, @@ -102,7 +113,7 @@ export const getDefaultControlsState = () => { /* while this may change, div currently doesn't have CIs, so they shouldn't be displayed. */ export const shouldDisplayTemporalConfidence = (exists, distMeasure, layout) => exists && distMeasure === "num_date" && layout === "rect"; -const Controls = (state = getDefaultControlsState(), action) => { +const Controls = (state: ControlsState = getDefaultControlsState(), action): ControlsState => { switch (action.type) { case types.URL_QUERY_CHANGE_WITH_COMPUTED_STATE: /* fallthrough */ case types.CLEAN_START: @@ -142,7 +153,7 @@ const Controls = (state = getDefaultControlsState(), action) => { }) }); case types.CHANGE_DISTANCE_MEASURE: { - const updatesToState = { + const updatesToState: Partial = { distanceMeasure: action.data, branchLengthsToDisplay: state.branchLengthsToDisplay }; @@ -161,7 +172,7 @@ const Controls = (state = getDefaultControlsState(), action) => { return Object.assign({}, state, updatesToState); } case types.CHANGE_DATES_VISIBILITY_THICKNESS: { - const newDates = { quickdraw: action.quickdraw }; + const newDates: Partial = { quickdraw: action.quickdraw }; if (action.dateMin) { newDates.dateMin = action.dateMin; newDates.dateMinNumeric = action.dateMinNumeric; @@ -257,7 +268,7 @@ const Controls = (state = getDefaultControlsState(), action) => { }); case types.REMOVE_TREE_TOO: return Object.assign({}, state, { - showTreeToo: undefined, + showTreeToo: false, showTangle: false, canTogglePanelLayout: hasMultipleGridPanels(state.panelsAvailable), panelsToDisplay: state.panelsAvailable.slice() From b8c65738ef9da277e70e29d016586af4b31e7132 Mon Sep 17 00:00:00 2001 From: Victor Lin <13424970+victorlin@users.noreply.github.com> Date: Tue, 10 Oct 2023 17:27:04 -0700 Subject: [PATCH 10/11] panel-toggles: Convert to TypeScript MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This uses the newly defined types in the controls reducer. The migration doc recommends replacing connect with the hooks API¹. ¹ https://redux.js.org/usage/migrating-to-modern-redux#migrating-connect-to-hooks --- src/components/controls/panel-toggles.tsx | 44 +++++++++++------------ 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/src/components/controls/panel-toggles.tsx b/src/components/controls/panel-toggles.tsx index 145eadd30..3339a07ea 100644 --- a/src/components/controls/panel-toggles.tsx +++ b/src/components/controls/panel-toggles.tsx @@ -1,35 +1,33 @@ import React from "react"; -import { connect } from "react-redux"; -import { withTranslation } from 'react-i18next'; +import { useDispatch, useSelector } from "react-redux"; +import { useTranslation } from 'react-i18next'; import Toggle from "./toggle"; import { togglePanelDisplay } from "../../actions/panelDisplay"; +import { RootState } from "../../store"; -@connect((state) => ({ - panelsAvailable: state.controls.panelsAvailable, - panelsToDisplay: state.controls.panelsToDisplay, - showTreeToo: state.controls.showTreeToo -})) -class PanelToggles extends React.Component { - render() { - const { t } = this.props; +export default function PanelToggles() { + const dispatch = useDispatch(); + const { t } = useTranslation(); - const panels = this.props.panelsAvailable.slice(); - if (this.props.showTreeToo && panels.indexOf("map") !== -1) { - panels.splice(panels.indexOf("map"), 1); - } - return panels.map((n) => ( + const panelsAvailable = useSelector((state: RootState) => state.controls.panelsAvailable); + const panelsToDisplay = useSelector((state: RootState) => state.controls.panelsToDisplay); + const showTreeToo = useSelector((state: RootState) => state.controls.showTreeToo); + + const panels = panelsAvailable.slice(); + if (showTreeToo && panels.indexOf("map") !== -1) { + panels.splice(panels.indexOf("map"), 1); + } + return <> + {panels.map((n) => ( this.props.dispatch(togglePanelDisplay(n))} + on={panelsToDisplay.indexOf(n) !== -1} + callback={() => dispatch(togglePanelDisplay(n))} label={t("sidebar:Show " + n)} - style={{paddingBottom: "10px"}} + style={{ paddingBottom: "10px" }} /> - )); - } + ))} + } - -const WithTranslation = withTranslation()(PanelToggles); -export default WithTranslation; From 3e77a0a0f572db0e4621d9dfc3614a7353311be2 Mon Sep 17 00:00:00 2001 From: Victor Lin <13424970+victorlin@users.noreply.github.com> Date: Wed, 11 Oct 2023 16:02:18 -0700 Subject: [PATCH 11/11] Use typed dispatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Recommended in migration guide¹. ¹ https://redux.js.org/usage/migrating-to-modern-redux#migrating-typescript-for-components --- src/components/controls/measurementsOptions.js | 5 +++-- src/components/controls/panel-toggles.tsx | 5 +++-- src/components/narrativeEditor/examineNarrative.js | 4 ++-- src/components/narrativeEditor/useDatasetFetch.js | 4 ++-- src/hooks.ts | 6 ++++++ 5 files changed, 16 insertions(+), 8 deletions(-) create mode 100644 src/hooks.ts diff --git a/src/components/controls/measurementsOptions.js b/src/components/controls/measurementsOptions.js index dee0fa21d..28f760013 100644 --- a/src/components/controls/measurementsOptions.js +++ b/src/components/controls/measurementsOptions.js @@ -1,5 +1,6 @@ import React from "react"; -import { useDispatch, useSelector } from "react-redux"; +import { useSelector } from "react-redux"; +import { useAppDispatch } from "../../hooks"; import { isEqual } from "lodash"; import { changeMeasurementsCollection } from "../../actions/measurements"; import { @@ -30,7 +31,7 @@ const collectionOptionsSelector = (collections) => { }; const MeasurementsOptions = () => { - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const collection = useSelector((state) => state.measurements.collectionToDisplay); const collectionOptions = useSelector((state) => collectionOptionsSelector(state.measurements.collections), isEqual); const groupBy = useSelector((state) => state.controls.measurementsGroupBy); diff --git a/src/components/controls/panel-toggles.tsx b/src/components/controls/panel-toggles.tsx index 3339a07ea..023ea9bae 100644 --- a/src/components/controls/panel-toggles.tsx +++ b/src/components/controls/panel-toggles.tsx @@ -1,5 +1,6 @@ import React from "react"; -import { useDispatch, useSelector } from "react-redux"; +import { useSelector } from "react-redux"; +import { useAppDispatch } from "../../hooks"; import { useTranslation } from 'react-i18next'; import Toggle from "./toggle"; @@ -7,7 +8,7 @@ import { togglePanelDisplay } from "../../actions/panelDisplay"; import { RootState } from "../../store"; export default function PanelToggles() { - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const { t } = useTranslation(); const panelsAvailable = useSelector((state: RootState) => state.controls.panelsAvailable); diff --git a/src/components/narrativeEditor/examineNarrative.js b/src/components/narrativeEditor/examineNarrative.js index 25eed58c4..8ee0b7bc8 100644 --- a/src/components/narrativeEditor/examineNarrative.js +++ b/src/components/narrativeEditor/examineNarrative.js @@ -1,5 +1,5 @@ import React, { useRef, useEffect } from "react"; -import { useDispatch } from 'react-redux'; +import { useAppDispatch } from "../../hooks"; import { getDatasetNamesFromUrl } from "../../actions/loadData"; import { CLEAN_START } from "../../actions/types"; import { createStateFromQueryOrJSONs } from "../../actions/recomputeReduxState"; @@ -159,7 +159,7 @@ const NarrativeSummary = ({summary, datasetResponses, showNarrative}) => { }; const ExamineNarrative = ({narrative, datasetResponses, setDisplayNarrative}) => { - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const el = useRef(null); useEffect(() => { /* when a narrative is loaded then we want to focus on however diff --git a/src/components/narrativeEditor/useDatasetFetch.js b/src/components/narrativeEditor/useDatasetFetch.js index 3c146f4fe..9ce53c3e2 100644 --- a/src/components/narrativeEditor/useDatasetFetch.js +++ b/src/components/narrativeEditor/useDatasetFetch.js @@ -1,5 +1,5 @@ import {useEffect, useReducer, useRef} from "react"; -import { useDispatch } from 'react-redux'; +import { useAppDispatch } from "../../hooks"; import { CACHE_JSONS } from "../../actions/types"; import { FetchError } from "../../util/exceptions"; @@ -14,7 +14,7 @@ import { FetchError } from "../../util/exceptions"; */ export function useDatasetFetch(datasets) { - const dispatchRedux = useDispatch(); + const dispatchRedux = useAppDispatch; const [datasetResponses, dispatchDatasetResponses] = useReducer( (state, action) => { if (action.reset) return {}; diff --git a/src/hooks.ts b/src/hooks.ts new file mode 100644 index 000000000..6a8507c8a --- /dev/null +++ b/src/hooks.ts @@ -0,0 +1,6 @@ +import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux' +import type { RootState, AppDispatch } from './store' + + +export const useAppDispatch: () => AppDispatch = useDispatch +export const useAppSelector: TypedUseSelectorHook = useSelector