From 8d26f768bfc2461318830d223b317f69b326efce Mon Sep 17 00:00:00 2001 From: David Lechner Date: Mon, 27 Dec 2021 17:37:43 -0600 Subject: [PATCH] reducers: use typed useSelector hook This refactors the code to use a typed useSelector() hook. --- src/app/App.tsx | 8 +++---- src/editor/Editor.tsx | 8 +++---- src/editor/OpenButton.tsx | 6 ++--- src/editor/SaveAsButton.tsx | 6 ++--- src/firmware/FlashButton.tsx | 14 +++++------- src/hub/BluetoothButton.tsx | 10 ++++----- src/hub/RunButton.tsx | 12 +++++----- src/hub/StopButton.tsx | 6 ++--- src/licenses/LicenseDialog.tsx | 8 +++---- src/reducers.ts | 6 +++++ src/settings/SettingsDrawer.tsx | 40 +++++++++++---------------------- src/status-bar/StatusBar.tsx | 19 ++++++---------- src/terminal/Terminal.tsx | 6 ++--- 13 files changed, 65 insertions(+), 84 deletions(-) diff --git a/src/app/App.tsx b/src/app/App.tsx index f9e3a9ee..60f7e211 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -3,10 +3,10 @@ import { Classes } from '@blueprintjs/core'; import React, { useEffect, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch } from 'react-redux'; import SplitterLayout from 'react-splitter-layout'; import Editor from '../editor/Editor'; -import { RootState } from '../reducers'; +import { useSelector } from '../reducers'; import { toggleBoolean } from '../settings/actions'; import { BooleanSettingId } from '../settings/defaults'; import StatusBar from '../status-bar/StatusBar'; @@ -122,8 +122,8 @@ const Docs: React.FunctionComponent = (_props) => { }; const App: React.FunctionComponent = (_props) => { - const darkMode = useSelector((s: RootState): boolean => s.settings.darkMode); - const showDocs = useSelector((s: RootState): boolean => s.settings.showDocs); + const darkMode = useSelector((s): boolean => s.settings.darkMode); + const showDocs = useSelector((s): boolean => s.settings.showDocs); const [isDragging, setIsDragging] = useState(false); // darkMode class has to be applied to body element, otherwise it won't diff --git a/src/editor/Editor.tsx b/src/editor/Editor.tsx index 435130a9..7c3e37c6 100644 --- a/src/editor/Editor.tsx +++ b/src/editor/Editor.tsx @@ -12,10 +12,10 @@ import tomorrowNightEightiesTheme from 'monaco-themes/themes/Tomorrow-Night-Eigh import xcodeTheme from 'monaco-themes/themes/Xcode_default.json'; import React, { useEffect, useRef } from 'react'; import MonacoEditor, { monaco } from 'react-monaco-editor'; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch } from 'react-redux'; import { IDisposable } from 'xterm'; import { compile } from '../mpy/actions'; -import { RootState } from '../reducers'; +import { useSelector } from '../reducers'; import { toggleBoolean } from '../settings/actions'; import { BooleanSettingId } from '../settings/defaults'; import { isMacOS } from '../utils/os'; @@ -63,7 +63,7 @@ const xcodeId = 'xcode'; monaco.editor.defineTheme(xcodeId, xcodeTheme as monaco.editor.IStandaloneThemeData); const contextMenu = (_props: ContextMenu2ContentProps): JSX.Element => { - const editor = useSelector((state: RootState) => state.editor.current); + const editor = useSelector((s) => s.editor.current); const [i18n] = useI18n({ id: 'editor', translations: { en }, fallback: en }); @@ -143,7 +143,7 @@ const Editor: React.FunctionComponent = (_props) => { return () => window.removeEventListener('storage', onStorage); }); - const darkMode = useSelector((state: RootState) => state.settings.darkMode); + const darkMode = useSelector((s) => s.settings.darkMode); const [i18n] = useI18n({ id: 'editor', translations: { en }, fallback: en }); diff --git a/src/editor/OpenButton.tsx b/src/editor/OpenButton.tsx index 0728701b..f5cf187e 100644 --- a/src/editor/OpenButton.tsx +++ b/src/editor/OpenButton.tsx @@ -2,9 +2,9 @@ // Copyright (c) 2020-2021 The Pybricks Authors import React from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch } from 'react-redux'; import * as notificationActions from '../notifications/actions'; -import { RootState } from '../reducers'; +import { useSelector } from '../reducers'; import OpenFileButton, { OpenFileButtonProps } from '../toolbar/OpenFileButton'; import { TooltipId } from '../toolbar/i18n'; import * as editorActions from './actions'; @@ -13,7 +13,7 @@ import openIcon from './open.svg'; type OpenButtonProps = Pick; const OpenButton: React.FunctionComponent = (props) => { - const editor = useSelector((state: RootState) => state.editor.current); + const editor = useSelector((s) => s.editor.current); const dispatch = useDispatch(); return ( diff --git a/src/editor/SaveAsButton.tsx b/src/editor/SaveAsButton.tsx index a676a2f3..d7c0d374 100644 --- a/src/editor/SaveAsButton.tsx +++ b/src/editor/SaveAsButton.tsx @@ -2,9 +2,9 @@ // Copyright (c) 2020-2021 The Pybricks Authors import React from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch } from 'react-redux'; import * as editorActions from '../editor/actions'; -import { RootState } from '../reducers'; +import { useSelector } from '../reducers'; import ActionButton, { ActionButtonProps } from '../toolbar/ActionButton'; import { TooltipId } from '../toolbar/i18n'; import downloadIcon from './save.svg'; @@ -13,7 +13,7 @@ type SaveAsButtonProps = Pick & Pick; const SaveAsButton: React.FunctionComponent = (props) => { - const editor = useSelector((state: RootState) => state.editor.current); + const editor = useSelector((s) => s.editor.current); const dispatch = useDispatch(); diff --git a/src/firmware/FlashButton.tsx b/src/firmware/FlashButton.tsx index 996a1fa9..4c319837 100644 --- a/src/firmware/FlashButton.tsx +++ b/src/firmware/FlashButton.tsx @@ -2,11 +2,11 @@ // Copyright (c) 2020-2021 The Pybricks Authors import React from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch } from 'react-redux'; import { BleConnectionState } from '../ble/reducers'; import { BootloaderConnectionState } from '../lwp3-bootloader/reducers'; import * as notificationActions from '../notifications/actions'; -import { RootState } from '../reducers'; +import { useSelector } from '../reducers'; import OpenFileButton, { OpenFileButtonProps } from '../toolbar/OpenFileButton'; import { TooltipId } from '../toolbar/i18n'; import { flashFirmware } from './actions'; @@ -15,12 +15,10 @@ import firmwareIcon from './firmware.svg'; type FlashButtonProps = Pick; const FlashButton: React.FunctionComponent = (props) => { - const bootloaderConnection = useSelector( - (state: RootState) => state.bootloader.connection, - ); - const bleConnection = useSelector((state: RootState) => state.ble.connection); - const flashing = useSelector((state: RootState) => state.firmware.flashing); - const progress = useSelector((state: RootState) => state.firmware.progress); + const bootloaderConnection = useSelector((s) => s.bootloader.connection); + const bleConnection = useSelector((s) => s.ble.connection); + const flashing = useSelector((s) => s.firmware.flashing); + const progress = useSelector((s) => s.firmware.progress); const dispatch = useDispatch(); diff --git a/src/hub/BluetoothButton.tsx b/src/hub/BluetoothButton.tsx index 3e91b38b..94eade97 100644 --- a/src/hub/BluetoothButton.tsx +++ b/src/hub/BluetoothButton.tsx @@ -2,11 +2,11 @@ // Copyright (c) 2020-2021 The Pybricks Authors import React from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch } from 'react-redux'; import { toggleBluetooth } from '../ble/actions'; import { BleConnectionState } from '../ble/reducers'; import { BootloaderConnectionState } from '../lwp3-bootloader/reducers'; -import { RootState } from '../reducers'; +import { useSelector } from '../reducers'; import ActionButton, { ActionButtonProps } from '../toolbar/ActionButton'; import { TooltipId } from '../toolbar/i18n'; import btConnectedIcon from './bt-connected.svg'; @@ -15,10 +15,8 @@ import btDisconnectedIcon from './bt-disconnected.svg'; type BluetoothButtonProps = Pick; const BluetoothButton: React.FunctionComponent = (props) => { - const bootloaderConnection = useSelector( - (state: RootState) => state.bootloader.connection, - ); - const bleConnection = useSelector((state: RootState) => state.ble.connection); + const bootloaderConnection = useSelector((s) => s.bootloader.connection); + const bleConnection = useSelector((s) => s.ble.connection); const isDisconnected = bootloaderConnection === BootloaderConnectionState.Disconnected && diff --git a/src/hub/RunButton.tsx b/src/hub/RunButton.tsx index 409e4719..33955423 100644 --- a/src/hub/RunButton.tsx +++ b/src/hub/RunButton.tsx @@ -2,8 +2,8 @@ // Copyright (c) 2020-2021 The Pybricks Authors import React from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { RootState } from '../reducers'; +import { useDispatch } from 'react-redux'; +import { useSelector } from '../reducers'; import ActionButton, { ActionButtonProps } from '../toolbar/ActionButton'; import { TooltipId } from '../toolbar/i18n'; import { downloadAndRun } from './actions'; @@ -14,11 +14,9 @@ type RunButtonProps = Pick & Pick; const RunButton: React.FunctionComponent = (props) => { - const editor = useSelector((state: RootState) => state.editor.current); - const downloadProgress = useSelector( - (state: RootState) => state.hub.downloadProgress, - ); - const runtime = useSelector((state: RootState) => state.hub.runtime); + const editor = useSelector((s) => s.editor.current); + const downloadProgress = useSelector((s) => s.hub.downloadProgress); + const runtime = useSelector((s) => s.hub.runtime); const dispatch = useDispatch(); diff --git a/src/hub/StopButton.tsx b/src/hub/StopButton.tsx index c433c0e2..69a24cf1 100644 --- a/src/hub/StopButton.tsx +++ b/src/hub/StopButton.tsx @@ -2,8 +2,8 @@ // Copyright (c) 2020-2021 The Pybricks Authors import React from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { RootState } from '../reducers'; +import { useDispatch } from 'react-redux'; +import { useSelector } from '../reducers'; import ActionButton, { ActionButtonProps } from '../toolbar/ActionButton'; import { TooltipId } from '../toolbar/i18n'; import { stop } from './actions'; @@ -14,7 +14,7 @@ type StopButtonProps = Pick & Pick; const StopButton: React.FunctionComponent = (props) => { - const runtime = useSelector((state: RootState) => state.hub.runtime); + const runtime = useSelector((s) => s.hub.runtime); const dispatch = useDispatch(); diff --git a/src/licenses/LicenseDialog.tsx b/src/licenses/LicenseDialog.tsx index d99356b8..d4d8ad8e 100644 --- a/src/licenses/LicenseDialog.tsx +++ b/src/licenses/LicenseDialog.tsx @@ -14,9 +14,9 @@ import { } from '@blueprintjs/core'; import { useI18n } from '@shopify/react-i18n'; import React from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch } from 'react-redux'; import { appName } from '../app/constants'; -import { RootState } from '../reducers'; +import { useSelector } from '../reducers'; import { fetchList, select } from './actions'; import { LicenseStringId } from './i18n'; import en from './i18n.en.json'; @@ -31,7 +31,7 @@ type LicenseListPanelProps = { const LicenseListPanel: React.VoidFunctionComponent = ( props, ) => { - const licenseList = useSelector((state: RootState) => state.licenses.list); + const licenseList = useSelector((s) => s.licenses.list); return (
@@ -54,7 +54,7 @@ const LicenseListPanel: React.VoidFunctionComponent = ( }; const LicenseInfoPanel = React.forwardRef((_props, ref) => { - const licenseInfo = useSelector((state: RootState) => state.licenses.selected); + const licenseInfo = useSelector((s) => s.licenses.selected); const [i18n] = useI18n({ id: 'license', translations: { en }, fallback: en }); diff --git a/src/reducers.ts b/src/reducers.ts index 04c5f38d..d01683df 100644 --- a/src/reducers.ts +++ b/src/reducers.ts @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT // Copyright (c) 2020-2021 The Pybricks Authors +import { TypedUseSelectorHook, useSelector as useReduxSelector } from 'react-redux'; import { Reducer, combineReducers } from 'redux'; import app from './app/reducers'; import ble from './ble/reducers'; @@ -31,3 +32,8 @@ export const rootReducer = combineReducers({ type StateFromReducer = R extends Reducer ? S : never; export type RootState = StateFromReducer; + +/** + * Typed version of react-redux useSelector() hook. + */ +export const useSelector: TypedUseSelectorHook = useReduxSelector; diff --git a/src/settings/SettingsDrawer.tsx b/src/settings/SettingsDrawer.tsx index 85221b76..7e21e08c 100644 --- a/src/settings/SettingsDrawer.tsx +++ b/src/settings/SettingsDrawer.tsx @@ -19,7 +19,7 @@ import { import { Tooltip2 } from '@blueprintjs/popover2'; import { useI18n } from '@shopify/react-i18n'; import React, { useMemo, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch } from 'react-redux'; import AboutDialog from '../about/AboutDialog'; import { checkForUpdate, installPrompt, reload } from '../app/actions'; import { @@ -30,7 +30,7 @@ import { tooltipDelay, } from '../app/constants'; import { pseudolocalize } from '../i18n'; -import { RootState } from '../reducers'; +import { useSelector } from '../reducers'; import ExternalLinkIcon from '../utils/ExternalLinkIcon'; import { isMacOS } from '../utils/os'; import { setBoolean, setString, toggleBoolean } from './actions'; @@ -47,31 +47,17 @@ type SettingsProps = { const SettingsDrawer: React.FunctionComponent = (props) => { const [isAboutDialogOpen, setIsAboutDialogOpen] = useState(false); - const showDocs = useSelector((state: RootState) => state.settings.showDocs); - const darkMode = useSelector((state: RootState) => state.settings.darkMode); - const flashCurrentProgram = useSelector( - (state: RootState) => state.settings.flashCurrentProgram, - ); - const serviceWorker = useSelector((state: RootState) => state.app.serviceWorker); - const checkingForUpdate = useSelector( - (state: RootState) => state.app.checkingForUpdate, - ); - const updateAvailable = useSelector( - (state: RootState) => state.app.updateAvailable, - ); - const beforeInstallPrompt = useSelector( - (state: RootState) => state.app.beforeInstallPrompt, - ); - const promptingInstall = useSelector( - (state: RootState) => state.app.promptingInstall, - ); - const readyForOfflineUse = useSelector( - (state: RootState) => state.app.readyForOfflineUse, - ); - const hubName = useSelector((state: RootState) => state.settings.hubName); - const isHubNameValid = useSelector( - (state: RootState) => state.settings.isHubNameValid, - ); + const showDocs = useSelector((s) => s.settings.showDocs); + const darkMode = useSelector((s) => s.settings.darkMode); + const flashCurrentProgram = useSelector((s) => s.settings.flashCurrentProgram); + const serviceWorker = useSelector((s) => s.app.serviceWorker); + const checkingForUpdate = useSelector((s) => s.app.checkingForUpdate); + const updateAvailable = useSelector((s) => s.app.updateAvailable); + const beforeInstallPrompt = useSelector((s) => s.app.beforeInstallPrompt); + const promptingInstall = useSelector((s) => s.app.promptingInstall); + const readyForOfflineUse = useSelector((s) => s.app.readyForOfflineUse); + const hubName = useSelector((s) => s.settings.hubName); + const isHubNameValid = useSelector((s) => s.settings.isHubNameValid); const dispatch = useDispatch(); diff --git a/src/status-bar/StatusBar.tsx b/src/status-bar/StatusBar.tsx index 017133c1..6f96493d 100644 --- a/src/status-bar/StatusBar.tsx +++ b/src/status-bar/StatusBar.tsx @@ -5,9 +5,8 @@ import { Button, Intent, ProgressBar } from '@blueprintjs/core'; import { Classes as Classes2, Popover2, Popover2Props } from '@blueprintjs/popover2'; import { useI18n } from '@shopify/react-i18n'; import React from 'react'; -import { useSelector } from 'react-redux'; import { BleConnectionState } from '../ble/reducers'; -import { RootState } from '../reducers'; +import { useSelector } from '../reducers'; import { MessageId } from './i18n'; import en from './i18n.en.json'; @@ -19,11 +18,9 @@ const commonPopoverProps: Partial = { }; const HubInfoButton: React.VFC = (_props) => { - const deviceName = useSelector((state: RootState) => state.ble.deviceName); - const deviceType = useSelector((state: RootState) => state.ble.deviceType); - const deviceFirmwareVersion = useSelector( - (state: RootState) => state.ble.deviceFirmwareVersion, - ); + const deviceName = useSelector((s) => s.ble.deviceName); + const deviceType = useSelector((s) => s.ble.deviceType); + const deviceFirmwareVersion = useSelector((s) => s.ble.deviceFirmwareVersion); const [i18n] = useI18n({ id: 'statusBar', translations: { en }, fallback: en }); @@ -69,10 +66,8 @@ const HubInfoButton: React.VFC = (_props) => { }; const BatteryIndicator: React.VFC = (_props) => { - const charging = useSelector((state: RootState) => state.ble.deviceBatteryCharging); - const lowBatteryWarning = useSelector( - (state: RootState) => state.ble.deviceLowBatteryWarning, - ); + const charging = useSelector((s) => s.ble.deviceBatteryCharging); + const lowBatteryWarning = useSelector((s) => s.ble.deviceLowBatteryWarning); const [i18n] = useI18n({ id: 'statusBar', translations: { en }, fallback: en }); @@ -102,7 +97,7 @@ const BatteryIndicator: React.VFC = (_props) => { }; const StatusBar: React.VFC = (_props) => { - const connection = useSelector((state: RootState) => state.ble.connection); + const connection = useSelector((s) => s.ble.connection); return (
{ const { xterm, fitAddon } = useMemo(createXTerm, [createXTerm]); const terminalRef = useRef(null); - const darkMode = useSelector((state: RootState) => state.settings.darkMode); + const darkMode = useSelector((s) => s.settings.darkMode); const dispatch = useDispatch(); const terminalStream = useContext(TerminalContext);