diff --git a/package.json b/package.json index 7c57e4a5..548b5f24 100644 --- a/package.json +++ b/package.json @@ -28,14 +28,15 @@ "pako": "2.1.0", "react": "18.2.0", "react-dom": "18.2.0", - "react-redux": "8.1.2", - "rxjs": "8.0.0-alpha.10" + "rxjs": "8.0.0-alpha.10", + "use-sync-external-store": "1.2.0" }, "devDependencies": { "@types/jest": "28.1.8", "@types/pako": "2.0.0", "@types/react": "18.2.18", "@types/react-dom": "18.2.7", + "@types/use-sync-external-store": "0.0.3", "@vitejs/plugin-react": "4.0.4", "jest": "28.1.3", "jest-environment-jsdom": "28.1.3", diff --git a/src/app/hooks.ts b/src/app/hooks.ts index 1c2fcc2d..d6f1c0b2 100644 --- a/src/app/hooks.ts +++ b/src/app/hooks.ts @@ -1,12 +1,22 @@ -import { - TypedUseSelectorHook, - useStore as __useStore, - useSelector as __useSelector -} from 'react-redux' -import type { RootState, Store } from './store' +import { useDebugValue } from 'react' +import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/with-selector' +import { RootStateSelector, store } from './store' -type TypedUseStoreHook = () => Store +type EqualityFn = (a: T, b: T) => boolean -export const useStore = __useStore as TypedUseStoreHook +const refEquality: EqualityFn = (a, b) => a === b -export const useSelector: TypedUseSelectorHook = __useSelector +export const useSelector = ( + selector: RootStateSelector, + equalityFn: EqualityFn = refEquality +): TSelected => { + const selectedState = useSyncExternalStoreWithSelector( + store.subscribe, + store.getState, + null, + selector, + equalityFn + ) + useDebugValue(selectedState) + return selectedState +} diff --git a/src/app/stateSaver.ts b/src/app/stateSaver.ts index 27c3bf8b..2c4c2274 100644 --- a/src/app/stateSaver.ts +++ b/src/app/stateSaver.ts @@ -1,5 +1,5 @@ import { useEffect } from 'react' -import { useStore } from './hooks' +import { store, applySelector } from './store' import { subscribe } from './subscribe' import { StateToPersist, selectStateToPersist } from './persist' import { saveState as saveStateToUrl } from './url' @@ -11,10 +11,8 @@ const saveState = (state: StateToPersist): void => { } export const useStateSaver = (): void => { - const store = useStore() - useEffect(() => { - const stateToPersist = selectStateToPersist(store.getState()) + const stateToPersist = applySelector(selectStateToPersist) saveState(stateToPersist) return subscribe(store.onState(selectStateToPersist), saveState) }, []) diff --git a/src/app/store.ts b/src/app/store.ts index 809c6ad5..bb051ad3 100644 --- a/src/app/store.ts +++ b/src/app/store.ts @@ -37,7 +37,7 @@ const getPreloadedState = (): Partial => { const actionObserver = createActionObserver() const stateObserver = createStateObserver() -const store = Object.assign( +export const store = Object.assign( configureStore({ reducer: rootReducer, middleware: getDefaultMiddleware => { @@ -54,4 +54,5 @@ const store = Object.assign( export type Store = typeof store -export default store +export const applySelector = (selector: RootStateSelector): TSelected => + selector(store.getState()) diff --git a/src/features/assembler/assemble.ts b/src/features/assembler/assemble.ts index 26116323..f9f2879d 100644 --- a/src/features/assembler/assemble.ts +++ b/src/features/assembler/assemble.ts @@ -1,4 +1,4 @@ -import type { Store } from '@/app/store' +import { store } from '@/app/store' import { AssembleResult, AssemblerError, assemble as assemblePure } from './core' import { setAssemblerState, setAssemblerError } from './assemblerSlice' import { setMemoryDataFrom } from '@/features/memory/memorySlice' @@ -6,34 +6,29 @@ import { resetCpuState } from '@/features/cpu/cpuSlice' import { setEditorHighlightRange, clearEditorHighlightRange } from '@/features/editor/editorSlice' import { setException } from '@/features/exception/exceptionSlice' -type Assemble = (input: string) => void - -export const createAssemble = (store: Store): Assemble => { - const assemble: Assemble = input => { - let assembleResult: AssembleResult - try { - assembleResult = assemblePure(input) - } catch (exception) { - if (exception instanceof AssemblerError) { - const assemblerErrorObject = exception.toPlainObject() - store.dispatch(setAssemblerError(assemblerErrorObject)) - store.dispatch(clearEditorHighlightRange()) - } else { - store.dispatch(setException(exception)) - } - return - } - const [addressToOpcodeMap, addressToStatementMap] = assembleResult - store.dispatch(setAssemblerState({ source: input, addressToStatementMap })) - store.dispatch(setMemoryDataFrom(addressToOpcodeMap)) - store.dispatch(resetCpuState()) - const firstStatement = addressToStatementMap[0] - const hasStatement = firstStatement !== undefined - if (hasStatement) { - store.dispatch(setEditorHighlightRange(firstStatement)) - } else { +export const assemble = (input: string): void => { + let assembleResult: AssembleResult + try { + assembleResult = assemblePure(input) + } catch (exception) { + if (exception instanceof AssemblerError) { + const assemblerErrorObject = exception.toPlainObject() + store.dispatch(setAssemblerError(assemblerErrorObject)) store.dispatch(clearEditorHighlightRange()) + } else { + store.dispatch(setException(exception)) } + return + } + const [addressToOpcodeMap, addressToStatementMap] = assembleResult + store.dispatch(setAssemblerState({ source: input, addressToStatementMap })) + store.dispatch(setMemoryDataFrom(addressToOpcodeMap)) + store.dispatch(resetCpuState()) + const firstStatement = addressToStatementMap[0] + const hasStatement = firstStatement !== undefined + if (hasStatement) { + store.dispatch(setEditorHighlightRange(firstStatement)) + } else { + store.dispatch(clearEditorHighlightRange()) } - return assemble } diff --git a/src/features/controller/ConfigurationMenu.tsx b/src/features/controller/ConfigurationMenu.tsx index 3bb921cb..d8f6df36 100644 --- a/src/features/controller/ConfigurationMenu.tsx +++ b/src/features/controller/ConfigurationMenu.tsx @@ -3,7 +3,8 @@ import MenuButton from './MenuButton' import MenuItems from './MenuItems' import MenuItem from './MenuItem' import { CheckMark, Wrench } from '@/common/components/icons' -import { useStore, useSelector } from '@/app/hooks' +import { store } from '@/app/store' +import { useSelector } from '@/app/hooks' import { ClockSpeed, clockSpeedOptionNames, @@ -18,7 +19,6 @@ import { } from './controllerSlice' const AutoAssembleSwitch = (): JSX.Element => { - const store = useStore() const autoAssemble = useSelector(selectAutoAssemble) const toggleAutoAssemble = (): void => { @@ -36,7 +36,6 @@ const AutoAssembleSwitch = (): JSX.Element => { } const ClockSpeedMenu = (): JSX.Element => { - const store = useStore() const clockSpeed = useSelector(selectClockSpeed) return ( @@ -74,7 +73,6 @@ const ClockSpeedMenu = (): JSX.Element => { } const TimerIntervalMenu = (): JSX.Element => { - const store = useStore() const timerInterval = useSelector(selectTimerInterval) return ( diff --git a/src/features/controller/FileMenu.tsx b/src/features/controller/FileMenu.tsx index 46b3b8c4..6830f3e3 100644 --- a/src/features/controller/FileMenu.tsx +++ b/src/features/controller/FileMenu.tsx @@ -5,30 +5,26 @@ import MenuItems from './MenuItems' import MenuItem from './MenuItem' import Modal from '@/common/components/Modal' import { File as FileIcon } from '@/common/components/icons' -import { useStore } from '@/app/hooks' +import { store, applySelector } from '@/app/store' import { setEditorInput, selectEditorInput } from '@/features/editor/editorSlice' import { template, examples } from '@/features/editor/examples' -const NewFileButton = (): JSX.Element => { - const store = useStore() - - return ( - { - store.dispatch( - setEditorInput({ - value: template.content, - isFromFile: true - }) - ) - }}> - - - New File - - - ) -} +const NewFileButton = (): JSX.Element => ( + { + store.dispatch( + setEditorInput({ + value: template.content, + isFromFile: true + }) + ) + }}> + + + New File + + +) interface OpenButtonProps { onFileLoad: () => void @@ -46,8 +42,6 @@ const OpenButton = ({ onFileLoad }: OpenButtonProps): JSX.Element => { event.stopPropagation() } - const store = useStore() - const loadFile = (file: File): void => { const reader = Object.assign(new FileReader(), { onload: () => { @@ -81,49 +75,43 @@ const OpenButton = ({ onFileLoad }: OpenButtonProps): JSX.Element => { ) } -const OpenExampleMenu = (): JSX.Element => { - const store = useStore() - - return ( - - {(isHovered, menuItemsRef, menuItemElement) => ( - <> - - - Open Example - - {isHovered && ( - - {examples.map(({ title, content }, index) => ( - { - store.dispatch( - setEditorInput({ - value: content, - isFromFile: true - }) - ) - }}> - - - {title} - - - ))} - - )} - - )} - - ) -} +const OpenExampleMenu = (): JSX.Element => ( + + {(isHovered, menuItemsRef, menuItemElement) => ( + <> + + + Open Example + + {isHovered && ( + + {examples.map(({ title, content }, index) => ( + { + store.dispatch( + setEditorInput({ + value: content, + isFromFile: true + }) + ) + }}> + + + {title} + + + ))} + + )} + + )} + +) const SaveButton = (): JSX.Element => { - const store = useStore() - const handleClick = (): void => { - const editorInput = selectEditorInput(store.getState()) + const editorInput = applySelector(selectEditorInput) const fileBlob = new Blob([editorInput], { type: 'application/octet-stream' }) const fileUrl = URL.createObjectURL(fileBlob) const anchorElement = Object.assign(document.createElement('a'), { diff --git a/src/features/controller/ViewMenu.tsx b/src/features/controller/ViewMenu.tsx index 5e6d011e..209759a7 100644 --- a/src/features/controller/ViewMenu.tsx +++ b/src/features/controller/ViewMenu.tsx @@ -3,13 +3,13 @@ import MenuButton from './MenuButton' import MenuItems from './MenuItems' import MenuItem from './MenuItem' import { CheckMark, View as ViewIcon } from '@/common/components/icons' -import { useStore, useSelector } from '@/app/hooks' +import { store } from '@/app/store' +import { useSelector } from '@/app/hooks' import { memoryViewOptions, selectMemoryView, setMemoryView } from '@/features/memory/memorySlice' import { ioDeviceNames, selectIoDeviceStates, toggleIoDeviceVisible } from '@/features/io/ioSlice' import { splitCamelCaseToString } from '@/common/utils' const MemoryMenu = (): JSX.Element => { - const store = useStore() const memoryView = useSelector(selectMemoryView) return ( @@ -43,7 +43,6 @@ const MemoryMenu = (): JSX.Element => { } const IoMenu = (): JSX.Element => { - const store = useStore() const ioDeviceStates = useSelector(selectIoDeviceStates) return ( diff --git a/src/features/controller/hooks.ts b/src/features/controller/hooks.ts index d792b50b..af3e5776 100644 --- a/src/features/controller/hooks.ts +++ b/src/features/controller/hooks.ts @@ -1,8 +1,7 @@ import { useEffect } from 'react' import { debounceTime, filter, first } from 'rxjs' -import type { RootStateSelector, Store } from '@/app/store' import { subscribe } from '@/app/subscribe' -import { useStore } from '@/app/hooks' +import { store, applySelector } from '@/app/store' import { selectRuntimeConfiguration, selectIsRunning, @@ -22,7 +21,7 @@ import { setEditorMessage } from '@/features/editor/editorSlice' import { lineRangesOverlap } from '@/features/editor/codemirror/text' -import { createAssemble } from '@/features/assembler/assemble' +import { assemble as assembleFrom } from '@/features/assembler/assemble' import { selectAssembledSource, selectIsAssembled, @@ -72,9 +71,6 @@ const sourceChangedMessage: EditorMessage = { } class Controller { - private readonly store: Store - private readonly applySelector: (selector: RootStateSelector) => T - private stepIntervalId?: number private interruptIntervalId?: number @@ -91,14 +87,8 @@ class Controller { private lastBreakpointLineNumber: number | undefined - constructor(store: Store) { - this.store = store - this.applySelector = selector => selector(store.getState()) - } - public assemble = (): void => { - const assembleFrom = createAssemble(this.store) - assembleFrom(this.applySelector(selectEditorInput)) + assembleFrom(applySelector(selectEditorInput)) } public runOrStop = async (): Promise => { @@ -111,7 +101,7 @@ class Controller { * @returns true if stopped from running */ private stopIfRunning(): boolean { - const isRunning = this.applySelector(selectIsRunning) + const isRunning = applySelector(selectIsRunning) if (isRunning) { this.stop() } @@ -123,7 +113,7 @@ class Controller { if (this.isInterruptIntervalSet) { this.clearInterruptInterval() } - this.store.dispatch(setRunning(false)) + store.dispatch(setRunning(false)) } private clearStepInterval(): void { @@ -138,7 +128,7 @@ class Controller { } private async run(): Promise { - this.store.dispatch(setRunning(true)) + store.dispatch(setRunning(true)) this.setStepInterval() const lastStepResult = await this.lastStep if (lastStepResult !== undefined) { @@ -151,14 +141,14 @@ class Controller { } private setStepInterval(): void { - const { clockSpeed } = this.applySelector(selectRuntimeConfiguration) + const { clockSpeed } = applySelector(selectRuntimeConfiguration) this.stepIntervalId = window.setInterval(this.step, 1000 / clockSpeed) } private setInterruptInterval(withFlag = true): void { - const { timerInterval } = this.applySelector(selectRuntimeConfiguration) + const { timerInterval } = applySelector(selectRuntimeConfiguration) this.interruptIntervalId = window.setInterval(() => { - this.store.dispatch(setInterrupt(true)) + store.dispatch(setInterrupt(true)) }, timerInterval) if (withFlag) { this.isInterruptIntervalSet = true @@ -188,17 +178,17 @@ class Controller { public step = async ({ isUserAction = false } = {}): Promise => { const lastStepResult = await this.lastStep if (isUserAction) { - this.store.dispatch(setInterrupt(false)) + store.dispatch(setInterrupt(false)) } - if (this.applySelector(selectEditorInput) !== this.applySelector(selectAssembledSource)) { - this.store.dispatch(setEditorMessage(sourceChangedMessage)) + if (applySelector(selectEditorInput) !== applySelector(selectAssembledSource)) { + store.dispatch(setEditorMessage(sourceChangedMessage)) } - const { fault, halted } = this.applySelector(selectCpuStatus) + const { fault, halted } = applySelector(selectCpuStatus) if (fault !== null || halted) { this.stopIfRunning() if (fault === null && halted) { // trigger `EditorMessage` re-render - this.store.dispatch(setCpuHalted()) + store.dispatch(setCpuHalted()) } return } @@ -207,25 +197,25 @@ class Controller { try { stepOutput = stepPure( lastStepResult ?? { - memoryData: this.applySelector(selectMemoryData), - cpuRegisters: this.applySelector(selectCpuRegisters) + memoryData: applySelector(selectMemoryData), + cpuRegisters: applySelector(selectCpuRegisters) }, - this.applySelector(selectInputSignals) + applySelector(selectInputSignals) ) } catch (exception) { this.stopIfRunning() if (exception instanceof RuntimeError) { const runtimeErrorObject = exception.toPlainObject() - this.store.dispatch(setCpuFault(runtimeErrorObject)) + store.dispatch(setCpuFault(runtimeErrorObject)) } else { - this.store.dispatch(setException(exception)) + store.dispatch(setException(exception)) } resolve(undefined) return } const { memoryData, cpuRegisters, signals, changes } = stepOutput const instructionAdress = cpuRegisters.ip - const addressToStatementMap = this.applySelector(selectAddressToStatementMap) + const addressToStatementMap = applySelector(selectAddressToStatementMap) const statement = addressToStatementMap[instructionAdress] const hasStatement = statement !== undefined && @@ -239,15 +229,15 @@ class Controller { const dispatchChanges = (): void => { this.cancelDispatchChanges() this.dispatchChangesTimeoutId = window.setTimeout(() => { - this.store.dispatch(setMemoryData(memoryData)) + store.dispatch(setMemoryData(memoryData)) if (isVduBufferChanged) { - this.store.dispatch(setVduDataFrom(memoryData)) + store.dispatch(setVduDataFrom(memoryData)) } - this.store.dispatch(setCpuRegisters(cpuRegisters)) + store.dispatch(setCpuRegisters(cpuRegisters)) if (hasStatement) { - this.store.dispatch(setEditorHighlightRange(statement)) + store.dispatch(setEditorHighlightRange(statement)) } else { - this.store.dispatch(clearEditorHighlightRange()) + store.dispatch(clearEditorHighlightRange()) } this.dispatchChangesTimeoutId = undefined }) @@ -255,7 +245,7 @@ class Controller { if (!this.willDispatchChanges || isVduBufferChanged) { dispatchChanges() } - const isRunning = this.applySelector(selectIsRunning) + const isRunning = applySelector(selectIsRunning) if (isRunning) { const isSrInterruptFlagChanged = changes.cpuRegisters?.sr?.interrupt ?? false if (isSrInterruptFlagChanged) { @@ -274,33 +264,33 @@ class Controller { const { data: inputData } = signals.input const { data: outputData, expectedInputPort } = signals.output if (signals.input.interrupt) { - this.store.dispatch(setInterrupt(false)) + store.dispatch(setInterrupt(false)) } if (signals.output.halted === true) { this.stopIfRunning() - this.store.dispatch(setCpuHalted()) + store.dispatch(setCpuHalted()) resolve(undefined) return } if (signals.output.closeWindows === true) { - this.store.dispatch(setIoDevicesInvisible()) + store.dispatch(setIoDevicesInvisible()) } let willSuspend = false if (expectedInputPort !== undefined) { - this.store.dispatch(setWaitingForInput(true)) + store.dispatch(setWaitingForInput(true)) if (inputData.content === null) { willSuspend = true if (isRunning) { this.pauseMainLoop() } - this.store.dispatch(setSuspended(true)) + store.dispatch(setSuspended(true)) switch (expectedInputPort) { case InputPort.SimulatedKeyboard: - this.store.dispatch(setWaitingForKeyboardInput(true)) + store.dispatch(setWaitingForKeyboardInput(true)) break } this.unsubscribeSetSuspended = subscribe( - this.store.onAction(setSuspended).pipe(first()), + store.onAction(setSuspended).pipe(first()), // payload must be false async () => { this.unsubscribeSetSuspended = undefined @@ -313,12 +303,12 @@ class Controller { ) } else { // wrong port - this.store.dispatch(clearInputData()) + store.dispatch(clearInputData()) } - } else if (this.applySelector(selectIsWaitingForInput)) { + } else if (applySelector(selectIsWaitingForInput)) { // `step` called from actionListener - this.store.dispatch(setWaitingForInput(false)) - this.store.dispatch(clearInputData()) + store.dispatch(setWaitingForInput(false)) + store.dispatch(clearInputData()) } if (outputData?.content !== undefined) { const { content: outputDataContent, port: outputPort } = outputData @@ -331,7 +321,7 @@ class Controller { } }) if (ioDeviceName !== undefined) { - this.store.dispatch( + store.dispatch( setIoDeviceData({ name: ioDeviceName, data: outputDataContent @@ -340,7 +330,7 @@ class Controller { } } let hasBreakpoint = false - const breakpoints = this.applySelector(selectEditorBreakpoints) + const breakpoints = applySelector(selectEditorBreakpoints) if (breakpoints.length > 0 && hasStatement && isRunning && !willSuspend) { const breakpointLineLoc = breakpoints.find(lineLoc => lineRangesOverlap(lineLoc, statement.range) @@ -377,11 +367,11 @@ class Controller { public reset = (): void => { this.resetSelf() - this.store.dispatch(resetMemoryData()) - this.store.dispatch(resetCpuState()) - this.store.dispatch(resetAssemblerState()) - this.store.dispatch(clearEditorHighlightRange()) - this.store.dispatch(resetIoState()) + store.dispatch(resetMemoryData()) + store.dispatch(resetCpuState()) + store.dispatch(resetAssemblerState()) + store.dispatch(clearEditorHighlightRange()) + store.dispatch(resetIoState()) } public resetSelf = (): void => { @@ -393,10 +383,10 @@ class Controller { } private restoreIfSuspended(): void { - if (this.applySelector(selectIsSuspended)) { + if (applySelector(selectIsSuspended)) { this.unsubscribeSetSuspended!() this.unsubscribeSetSuspended = undefined - this.store.dispatch(setSuspended(false)) + store.dispatch(setSuspended(false)) } } @@ -406,8 +396,7 @@ class Controller { } export const useController = (): Controller => { - const store = useStore() - const controller = useSingleton(() => new Controller(store)) + const controller = useSingleton(() => new Controller()) useEffect(() => { return subscribe(store.onAction(setEditorInput), controller.resetSelf) @@ -417,7 +406,7 @@ export const useController = (): Controller => { return subscribe( store.onAction(setAutoAssemble).pipe( debounceTime(UPDATE_TIMEOUT_MS), - filter(shouldAutoAssemble => shouldAutoAssemble && !selectIsAssembled(store.getState())) + filter(shouldAutoAssemble => shouldAutoAssemble && !applySelector(selectIsAssembled)) ), controller.assemble ) @@ -431,10 +420,9 @@ export const useController = (): Controller => { return subscribe( store.onState(selectRuntimeConfiguration).pipe( filter(() => { - const state = store.getState() // `setSuspended` action listener will resume the main loop with new configuration // so we skip calling `stopAndRun` if cpu is suspended - return selectIsRunning(state) && !selectIsSuspended(state) + return applySelector(selectIsRunning) && !applySelector(selectIsSuspended) }) ), controller.stopAndRun diff --git a/src/features/editor/Editor.tsx b/src/features/editor/Editor.tsx index 25d865cb..a7211306 100644 --- a/src/features/editor/Editor.tsx +++ b/src/features/editor/Editor.tsx @@ -3,17 +3,15 @@ import type { CodeMirrorConfig } from '@codemirror-toolkit/react' import { CodeMirrorProvider } from './codemirror/react' import CodeMirrorContainer from './CodeMirrorContainer' import EditorMessage from './EditorMessage' -import { useStore } from '@/app/hooks' +import { store, applySelector } from '@/app/store' import { selectEditorInput } from './editorSlice' import { getSetup } from './codemirror/setup' import { exceptionSink } from './codemirror/exceptionSink' import { setException } from '@/features/exception/exceptionSlice' const Editor = (): JSX.Element => { - const store = useStore() - const codeMirrorConfig = useMemo(() => { - const editorInput = selectEditorInput(store.getState()) + const editorInput = applySelector(selectEditorInput) return { doc: editorInput, extensions: [ diff --git a/src/features/editor/hooks.ts b/src/features/editor/hooks.ts index 41ca60a8..fccec760 100644 --- a/src/features/editor/hooks.ts +++ b/src/features/editor/hooks.ts @@ -3,7 +3,8 @@ import { filter } from 'rxjs' import { addUpdateListener } from '@codemirror-toolkit/extensions' import { rangeSetsEqual, mapRangeSetToArray } from '@codemirror-toolkit/utils' import { subscribe } from '@/app/subscribe' -import { useStore, useSelector } from '@/app/hooks' +import { store, applySelector } from '@/app/store' +import { useSelector } from '@/app/hooks' import { MessageType, EditorMessage, @@ -26,14 +27,13 @@ import { BreakpointEffect, getBreakpointMarkers } from './codemirror/breakpoints import { withStringAnnotation, hasStringAnnotation } from './codemirror/annotations' import { lineLocAt, lineRangesEqual } from './codemirror/text' import { selectAutoAssemble } from '@/features/controller/controllerSlice' -import { createAssemble } from '@/features/assembler/assemble' +import { assemble } from '@/features/assembler/assemble' import { selectAssemblerError, selectAssemblerErrorRange, clearAssemblerError } from '@/features/assembler/assemblerSlice' import { selectCpuFault, setCpuHalted, resetCpuState } from '@/features/cpu/cpuSlice' -import { useSingleton } from '@/common/hooks' import { UPDATE_TIMEOUT_MS } from '@/common/constants' enum AnnotationValue { @@ -44,8 +44,6 @@ const syncFromState = withStringAnnotation(AnnotationValue.SyncFromState) const isSyncFromState = hasStringAnnotation(AnnotationValue.SyncFromState) export const useSyncInput = (): void => { - const store = useStore() - useViewEffect(view => { let syncInputTimeoutId: number | undefined return addUpdateListener(view, update => { @@ -89,8 +87,6 @@ export const useSyncInput = (): void => { } export const useAutoFocus = (): void => { - const store = useStore() - useViewEffect(view => { return subscribe( store @@ -112,12 +108,9 @@ export const useAutoFocus = (): void => { } export const useAutoAssemble = (): void => { - const store = useStore() - const assemble = useSingleton(() => createAssemble(store)) - useViewEffect(view => { let initialAssembleTimeoutId: number | undefined = window.setTimeout(() => { - if (selectAutoAssemble(store.getState())) { + if (applySelector(selectAutoAssemble)) { const input = view.state.doc.toString() assemble(input) } @@ -132,7 +125,7 @@ export const useAutoAssemble = (): void => { useEffect(() => { return subscribe(store.onAction(setEditorInput), ({ value, isFromFile }) => { - if (selectAutoAssemble(store.getState())) { + if (applySelector(selectAutoAssemble)) { if (isFromFile) { window.setTimeout(() => { assemble(value) @@ -146,11 +139,9 @@ export const useAutoAssemble = (): void => { } export const useAssemblerError = (): void => { - const store = useStore() - useViewEffect(view => { return addUpdateListener(view, update => { - if (update.docChanged && selectAssemblerError(store.getState()) !== null) { + if (update.docChanged && applySelector(selectAssemblerError) !== null) { store.dispatch(clearAssemblerError()) } }) @@ -170,8 +161,6 @@ export const useAssemblerError = (): void => { } export const useHighlightLine = (): void => { - const store = useStore() - useEffect(() => { return subscribe( store.onAction(setEditorInput).pipe(filter(({ isFromFile }) => isFromFile)), @@ -207,8 +196,6 @@ export const useHighlightLine = (): void => { } export const useBreakpoints = (): void => { - const store = useStore() - useViewEffect(view => { return addUpdateListener(view, update => { if (update.docChanged) { @@ -237,7 +224,7 @@ export const useBreakpoints = (): void => { }, []) useViewEffect(view => { - const breakpoints = selectEditorBreakpoints(store.getState()) + const breakpoints = applySelector(selectEditorBreakpoints) // persisted state might not be in sync with codemirror const validBreakpoints = breakpoints.filter( lineLoc => @@ -281,8 +268,6 @@ const errorToMessage = (error: Error): EditorMessage => { } export const useMessage = (): EditorMessage | null => { - const store = useStore() - const assemblerError = useSelector(selectAssemblerError) const runtimeError = useSelector(selectCpuFault) @@ -313,7 +298,7 @@ export const useMessage = (): EditorMessage | null => { return subscribe( store .onAction(resetCpuState) - .pipe(filter(() => selectEditorMessage(store.getState()) === haltedMessage)), + .pipe(filter(() => applySelector(selectEditorMessage) === haltedMessage)), () => { window.clearTimeout(messageTimeoutIdRef.current) messageTimeoutIdRef.current = undefined diff --git a/src/features/exception/ErrorBoundary.tsx b/src/features/exception/ErrorBoundary.tsx index 77318896..8d421de1 100644 --- a/src/features/exception/ErrorBoundary.tsx +++ b/src/features/exception/ErrorBoundary.tsx @@ -1,5 +1,5 @@ import { ReactNode, Component, useCallback } from 'react' -import { useStore } from '@/app/hooks' +import { store } from '@/app/store' import { setException } from './exceptionSlice' type ErrorHandler = (error: Error) => void @@ -30,8 +30,6 @@ interface Props { } const ErrorBoundary = ({ children }: Props): JSX.Element => { - const store = useStore() - const handleError = useCallback(error => { store.dispatch(setException(error)) }, []) diff --git a/src/features/exception/ExceptionModal.tsx b/src/features/exception/ExceptionModal.tsx index 79a96d4f..465ea3d8 100644 --- a/src/features/exception/ExceptionModal.tsx +++ b/src/features/exception/ExceptionModal.tsx @@ -2,12 +2,11 @@ import { useCallback } from 'react' import Modal from '@/common/components/Modal' import Anchor from '@/common/components/Anchor' import { selectException, clearException } from './exceptionSlice' -import { useStore, useSelector } from '@/app/hooks' +import { store } from '@/app/store' +import { useSelector } from '@/app/hooks' import { useOutsideClick } from '@/common/hooks' const ExceptionModal = (): JSX.Element => { - const store = useStore() - const error = useSelector(selectException) const hasError = error !== null diff --git a/src/features/exception/hooks.ts b/src/features/exception/hooks.ts index 7025ddc5..850e3f54 100644 --- a/src/features/exception/hooks.ts +++ b/src/features/exception/hooks.ts @@ -1,10 +1,8 @@ import { useEffect } from 'react' -import { useStore } from '@/app/hooks' +import { store } from '@/app/store' import { setException } from './exceptionSlice' export const useGlobalExceptionHandler = (): void => { - const store = useStore() - useEffect(() => { const handleError = (event: ErrorEvent): void => { store.dispatch(setException(event.error)) diff --git a/src/features/io/SevenSegmentDisplay.tsx b/src/features/io/SevenSegmentDisplay.tsx index b804e3f0..f08b252f 100644 --- a/src/features/io/SevenSegmentDisplay.tsx +++ b/src/features/io/SevenSegmentDisplay.tsx @@ -1,7 +1,7 @@ import { memo, useState, useEffect } from 'react' import { createNextState } from '@reduxjs/toolkit' import DeviceCard from './DeviceCard' -import { useStore } from '@/app/hooks' +import { store } from '@/app/store' import { subscribe } from '@/app/subscribe' import { IoDeviceName, resetIoState } from './ioSlice' import { useIoDevice } from './hooks' @@ -136,8 +136,6 @@ const DATA_DIGIT_COUNT = 14 const initialData = new Array(DATA_DIGIT_COUNT).fill(0) const SevenSegmentDisplay = (): JSX.Element | null => { - const store = useStore() - // an array of 14 digits, // elements with even index represent the left part const [data, setData] = useState(initialData) diff --git a/src/features/io/SimulatedKeyboard.tsx b/src/features/io/SimulatedKeyboard.tsx index 96061c4a..290c8e33 100644 --- a/src/features/io/SimulatedKeyboard.tsx +++ b/src/features/io/SimulatedKeyboard.tsx @@ -1,6 +1,7 @@ import { useRef } from 'react' import Modal from '@/common/components/Modal' -import { useStore, useSelector } from '@/app/hooks' +import { store } from '@/app/store' +import { useSelector } from '@/app/hooks' import { selectIsSuspended, setSuspended } from '@/features/controller/controllerSlice' import { selectIsWaitingForKeyboardInput, @@ -11,8 +12,6 @@ import { InputPort, SKIP } from './core' import { Ascii } from '@/common/constants' const SimulatedKeyboard = (): JSX.Element | null => { - const store = useStore() - const isSuspended = useSelector(selectIsSuspended) const isWaitingForKeyboardInput = useSelector(selectIsWaitingForKeyboardInput) const shouldOpen = isSuspended && isWaitingForKeyboardInput diff --git a/src/features/io/hooks.ts b/src/features/io/hooks.ts index 6592f583..dfd6056d 100644 --- a/src/features/io/hooks.ts +++ b/src/features/io/hooks.ts @@ -1,7 +1,8 @@ import { useEffect, useCallback, useMemo } from 'react' import { filter } from 'rxjs' import { Unsubscribe, subscribe } from '@/app/subscribe' -import { useStore, useSelector } from '@/app/hooks' +import { store, applySelector } from '@/app/store' +import { useSelector } from '@/app/hooks' import { IoDeviceName, IoDeviceState, @@ -21,8 +22,6 @@ interface IoDeviceActions { interface IoDevice extends IoDeviceState, IoDeviceActions {} export const useIoDevice = (deviceName: IoDeviceName): IoDevice => { - const store = useStore() - const selectData = useMemo(() => selectIoDeviceData(deviceName), [deviceName]) const data = useSelector(selectData) @@ -56,12 +55,11 @@ export const useIoDevice = (deviceName: IoDeviceName): IoDevice => { } export const useVisualDisplayUnit = (): ReturnType => { - const store = useStore() const device = useIoDevice(IoDeviceName.VisualDisplayUnit) useEffect(() => { return subscribe(store.onAction(setMemoryDataFrom), () => { - const memoryData = selectMemoryData(store.getState()) + const memoryData = applySelector(selectMemoryData) store.dispatch(setVduDataFrom(memoryData)) }) }, []) diff --git a/src/main.tsx b/src/main.tsx index 6b560447..0cc6baba 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,9 +1,7 @@ import * as React from 'react' import * as ReactDOM from 'react-dom/client' -import * as ReactRedux from 'react-redux' import 'virtual:windi.css' import './styles.css' -import store from './app/store' import App from './app/App' const container = document.getElementById('app-root')! @@ -11,8 +9,6 @@ const root = ReactDOM.createRoot(container) root.render( - - - + ) diff --git a/yarn.lock b/yarn.lock index 0a87c0ef..fdcee3fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1353,7 +1353,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.8.4": +"@babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.8.4": version: 7.22.6 resolution: "@babel/runtime@npm:7.22.6" dependencies: @@ -2300,16 +2300,6 @@ __metadata: languageName: node linkType: hard -"@types/hoist-non-react-statics@npm:^3.3.1": - version: 3.3.1 - resolution: "@types/hoist-non-react-statics@npm:3.3.1" - dependencies: - "@types/react": "*" - hoist-non-react-statics: ^3.3.0 - checksum: 2c0778570d9a01d05afabc781b32163f28409bb98f7245c38d5eaf082416fdb73034003f5825eb5e21313044e8d2d9e1f3fe2831e345d3d1b1d20bcd12270719 - languageName: node - linkType: hard - "@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0, @types/istanbul-lib-coverage@npm:^2.0.1": version: 2.0.4 resolution: "@types/istanbul-lib-coverage@npm:2.0.4" @@ -2462,7 +2452,7 @@ __metadata: languageName: node linkType: hard -"@types/use-sync-external-store@npm:^0.0.3": +"@types/use-sync-external-store@npm:0.0.3": version: 0.0.3 resolution: "@types/use-sync-external-store@npm:0.0.3" checksum: 161ddb8eec5dbe7279ac971531217e9af6b99f7783213566d2b502e2e2378ea19cf5e5ea4595039d730aa79d3d35c6567d48599f69773a02ffcff1776ec2a44e @@ -2921,6 +2911,7 @@ __metadata: "@types/pako": 2.0.0 "@types/react": 18.2.18 "@types/react-dom": 18.2.7 + "@types/use-sync-external-store": 0.0.3 "@vitejs/plugin-react": 4.0.4 immer: 10.0.2 jest: 28.1.3 @@ -2929,12 +2920,12 @@ __metadata: pako: 2.1.0 react: 18.2.0 react-dom: 18.2.0 - react-redux: 8.1.2 rxjs: 8.0.0-alpha.10 ts-jest: 28.0.8 ts-standardx: 0.8.4 ts-toolbelt: 9.6.0 typescript: 4.4.4 + use-sync-external-store: 1.2.0 vite: 4.4.8 vite-plugin-pwa: 0.16.4 vite-plugin-windicss: 1.9.0 @@ -4742,15 +4733,6 @@ __metadata: languageName: node linkType: hard -"hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.2": - version: 3.3.2 - resolution: "hoist-non-react-statics@npm:3.3.2" - dependencies: - react-is: ^16.7.0 - checksum: b1538270429b13901ee586aa44f4cc3ecd8831c061d06cb8322e50ea17b3f5ce4d0e2e66394761e6c8e152cd8c34fb3b4b690116c6ce2bd45b18c746516cb9e8 - languageName: node - linkType: hard - "hosted-git-info@npm:^2.1.4": version: 2.8.9 resolution: "hosted-git-info@npm:2.8.9" @@ -6899,7 +6881,7 @@ __metadata: languageName: node linkType: hard -"react-is@npm:^16.13.1, react-is@npm:^16.7.0": +"react-is@npm:^16.13.1": version: 16.13.1 resolution: "react-is@npm:16.13.1" checksum: f7a19ac3496de32ca9ae12aa030f00f14a3d45374f1ceca0af707c831b2a6098ef0d6bdae51bd437b0a306d7f01d4677fcc8de7c0d331eb47ad0f46130e53c5f @@ -6913,38 +6895,6 @@ __metadata: languageName: node linkType: hard -"react-redux@npm:8.1.2": - version: 8.1.2 - resolution: "react-redux@npm:8.1.2" - dependencies: - "@babel/runtime": ^7.12.1 - "@types/hoist-non-react-statics": ^3.3.1 - "@types/use-sync-external-store": ^0.0.3 - hoist-non-react-statics: ^3.3.2 - react-is: ^18.0.0 - use-sync-external-store: ^1.0.0 - peerDependencies: - "@types/react": ^16.8 || ^17.0 || ^18.0 - "@types/react-dom": ^16.8 || ^17.0 || ^18.0 - react: ^16.8 || ^17.0 || ^18.0 - react-dom: ^16.8 || ^17.0 || ^18.0 - react-native: ">=0.59" - redux: ^4 || ^5.0.0-beta.0 - peerDependenciesMeta: - "@types/react": - optional: true - "@types/react-dom": - optional: true - react-dom: - optional: true - react-native: - optional: true - redux: - optional: true - checksum: 4d5976b0f721e4148475871fcabce2fee875cc7f70f9a292f3370d63b38aa1dd474eb303c073c5555f3e69fc732f3bac05303def60304775deb28361e3f4b7cc - languageName: node - linkType: hard - "react-refresh@npm:^0.14.0": version: 0.14.0 resolution: "react-refresh@npm:0.14.0" @@ -8267,7 +8217,7 @@ __metadata: languageName: node linkType: hard -"use-sync-external-store@npm:^1.0.0, use-sync-external-store@npm:^1.2.0": +"use-sync-external-store@npm:1.2.0, use-sync-external-store@npm:^1.2.0": version: 1.2.0 resolution: "use-sync-external-store@npm:1.2.0" peerDependencies: