diff --git a/src/App/App.tsx b/src/App/App.tsx index ea8e514..9b77e2e 100644 --- a/src/App/App.tsx +++ b/src/App/App.tsx @@ -5,16 +5,18 @@ import { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from import { detectConnections } from "@/statecharts/detect_connections"; import { parseStatechart } from "../statecharts/parser"; -import { BottomPanel } from "./BottomPanel"; -import { defaultSideBarState, SideBar, SideBarState } from "./SideBar"; +import { BottomPanel } from "./BottomPanel/BottomPanel"; +import { defaultSideBarState, SideBar, SideBarState } from "./SideBar/SideBar"; import { InsertMode } from "./TopPanel/InsertModes"; import { TopPanel } from "./TopPanel/TopPanel"; import { VisualEditor, VisualEditorState } from "./VisualEditor/VisualEditor"; -import { makeIndividualSetters } from "./makePartialSetter"; -import { useEditor } from "./useEditor"; -import { useSimulator } from "./useSimulator"; -import { useUrlHashState } from "./useUrlHashState"; +import { makeAllSetters } from "./makePartialSetter"; +import { useEditor } from "./hooks/useEditor"; +import { useSimulator } from "./hooks/useSimulator"; +import { useUrlHashState } from "../hooks/useUrlHashState"; import { plants } from "./plants"; +import { emptyState } from "@/statecharts/concrete_syntax"; +import { ModalOverlay } from "./Modals/ModalOverlay"; export type EditHistory = { current: VisualEditorState, @@ -56,9 +58,12 @@ export function App() { const persist = useUrlHashState( recoveredState => { + if (recoveredState === null) { + setEditHistory(() => ({current: emptyState, history: [], future: []})); + } // we support two formats // @ts-ignore - if (recoveredState.nextID) { + else if (recoveredState.nextID) { // old format setEditHistory(() => ({current: recoveredState as VisualEditorState, history: [], future: []})); } @@ -77,6 +82,7 @@ export function App() { useEffect(() => { const timeout = setTimeout(() => { if (editorState !== null) { + console.log('persisting state to url'); persist({editorState, ...appState}); } }, 100); @@ -104,7 +110,15 @@ export function App() { const simulator = useSimulator(ast, plant, plantConns, scrollDownSidebar); - const setters = makeIndividualSetters(setAppState, Object.keys(appState) as (keyof AppState)[]); + // console.log('render app', {ast, plant, appState}); + // useDetectChange(ast, 'ast'); + // useDetectChange(plant, 'plant'); + // useDetectChange(scrollDownSidebar, 'scrollDownSidebar'); + // useDetectChange(appState, 'appState'); + // useDetectChange(simulator.time, 'simulator.time'); + // useDetectChange(simulator.trace, 'simulator.trace'); + + const setters = makeAllSetters(setAppState, Object.keys(appState) as (keyof AppState)[]); const syntaxErrors = parsed && parsed[1] || []; const currentTraceItem = simulator.trace && simulator.trace.trace[simulator.trace.idx]; @@ -121,63 +135,51 @@ export function App() { const plantState = currentBigStep && currentBigStep.state.plant || plant.execution.initial()[1]; - return <> + return + {/* top-to-bottom: everything -> bottom panel */} +
- {/* Modal dialog */} - {modal &&
setModal(null)}> -
- e.stopPropagation()}> - {modal} - -
-
} + {/* left-to-right: main -> sidebar */} +
- {/* top-to-bottom: everything -> bottom panel */} -
- - {/* left-to-right: main -> sidebar */} -
- - {/* top-to-bottom: top bar, editor */} -
- {/* Top bar */} -
- {editHistory && } + {/* top-to-bottom: top bar, editor */} +
+ {/* Top bar */} +
+ {editHistory && } +
+ {/* Editor */} +
+ {editorState && conns && syntaxErrors && + } +
- {/* Editor */} -
- {editorState && conns && syntaxErrors && - } + + {/* Right: sidebar */} +
+
+ +
- {/* Right: sidebar */} -
-
- -
+ {/* Bottom panel */} +
+ {syntaxErrors && }
- - {/* Bottom panel */} -
- {syntaxErrors && } -
-
- ; + ; } export default App; diff --git a/src/App/BottomPanel.css b/src/App/BottomPanel/BottomPanel.css similarity index 100% rename from src/App/BottomPanel.css rename to src/App/BottomPanel/BottomPanel.css diff --git a/src/App/BottomPanel.tsx b/src/App/BottomPanel/BottomPanel.tsx similarity index 84% rename from src/App/BottomPanel.tsx rename to src/App/BottomPanel/BottomPanel.tsx index 2f84992..c0d02ae 100644 --- a/src/App/BottomPanel.tsx +++ b/src/App/BottomPanel/BottomPanel.tsx @@ -1,10 +1,10 @@ import { useEffect, useState } from "react"; -import { TraceableError } from "../statecharts/parser"; +import { TraceableError } from "../../statecharts/parser"; import "./BottomPanel.css"; -import logo from "../../artwork/logo-playful.svg"; -import { PersistentDetailsLocalStorage } from "./PersistentDetails"; +import logo from "../../../artwork/logo-playful.svg"; +import { PersistentDetailsLocalStorage } from "../PersistentDetails"; export function BottomPanel(props: {errors: TraceableError[]}) { const [greeting, setGreeting] = useState( diff --git a/src/App/Modals/ModalOverlay.tsx b/src/App/Modals/ModalOverlay.tsx new file mode 100644 index 0000000..965879d --- /dev/null +++ b/src/App/Modals/ModalOverlay.tsx @@ -0,0 +1,17 @@ +import { Dispatch, PropsWithChildren, ReactElement, SetStateAction } from "react"; + +export function ModalOverlay(props: PropsWithChildren<{modal: ReactElement|null, setModal: Dispatch>}>) { + return <> + {props.modal &&
props.setModal(null)}> +
+ e.stopPropagation()}> + {props.modal} + +
+
} + + {props.children} + ; +} diff --git a/src/App/PersistentDetails.tsx b/src/App/PersistentDetails.tsx index 4d24bb0..c2f553b 100644 --- a/src/App/PersistentDetails.tsx +++ b/src/App/PersistentDetails.tsx @@ -1,4 +1,4 @@ -import { usePersistentState } from "@/App/persistent_state" +import { usePersistentState } from "@/hooks/usePersistentState" import { DetailsHTMLAttributes, Dispatch, PropsWithChildren, SetStateAction } from "react"; type Props = { diff --git a/src/App/Plant/DigitalWatch/DigitalWatch.tsx b/src/App/Plant/DigitalWatch/DigitalWatch.tsx index 9b41f94..3cb5f55 100644 --- a/src/App/Plant/DigitalWatch/DigitalWatch.tsx +++ b/src/App/Plant/DigitalWatch/DigitalWatch.tsx @@ -1,4 +1,4 @@ -import { useAudioContext } from "@/App/useAudioContext"; +import { useAudioContext } from "@/hooks/useAudioContext"; import { ConcreteSyntax } from "@/App/VisualEditor/VisualEditor"; import { detectConnections } from "@/statecharts/detect_connections"; import { parseStatechart } from "@/statecharts/parser"; diff --git a/src/App/Plant/Microwave/Microwave.tsx b/src/App/Plant/Microwave/Microwave.tsx index 516b444..bc50993 100644 --- a/src/App/Plant/Microwave/Microwave.tsx +++ b/src/App/Plant/Microwave/Microwave.tsx @@ -12,7 +12,7 @@ import { RT_Statechart } from "@/statecharts/runtime_types"; import { memo, useEffect } from "react"; import "./Microwave.css"; -import { useAudioContext } from "../../useAudioContext"; +import { useAudioContext } from "../../../hooks/useAudioContext"; import { makeStatechartPlant, PlantRenderProps, StatechartPlantSpec } from "../Plant"; import { detectConnections } from "@/statecharts/detect_connections"; import { parseStatechart } from "@/statecharts/parser"; diff --git a/src/App/Plant/Plant.ts b/src/App/Plant/Plant.ts index 444c848..afa26ef 100644 --- a/src/App/Plant/Plant.ts +++ b/src/App/Plant/Plant.ts @@ -3,7 +3,6 @@ import { Statechart } from "@/statecharts/abstract_syntax"; import { EventTrigger } from "@/statecharts/label_ast"; import { BigStep, RaisedEvent, RT_Statechart } from "@/statecharts/runtime_types"; import { statechartExecution, TimedReactive } from "@/statecharts/timed_reactive"; -import { setsEqual } from "@/util/util"; export type PlantRenderProps = { state: StateType, diff --git a/src/App/Plant/TrafficLight/TrafficLight.tsx b/src/App/Plant/TrafficLight/TrafficLight.tsx index fb47f48..5c5c833 100644 --- a/src/App/Plant/TrafficLight/TrafficLight.tsx +++ b/src/App/Plant/TrafficLight/TrafficLight.tsx @@ -13,7 +13,7 @@ import { ConcreteSyntax } from "@/App/VisualEditor/VisualEditor"; import { detectConnections } from "@/statecharts/detect_connections"; import { makeStatechartPlant, PlantRenderProps, StatechartPlantSpec } from "../Plant"; import { RT_Statechart } from "@/statecharts/runtime_types"; -import { useAudioContext } from "@/App/useAudioContext"; +import { useAudioContext } from "@/hooks/useAudioContext"; import { memo, useEffect } from "react"; import { objectsEqual } from "@/util/util"; diff --git a/src/App/RTHistory.tsx b/src/App/SideBar/RTHistory.tsx similarity index 94% rename from src/App/RTHistory.tsx rename to src/App/SideBar/RTHistory.tsx index 938867d..7e69844 100644 --- a/src/App/RTHistory.tsx +++ b/src/App/SideBar/RTHistory.tsx @@ -1,10 +1,10 @@ import { Dispatch, memo, SetStateAction, useCallback } from "react"; -import { Statechart, stateDescription } from "../statecharts/abstract_syntax"; -import { Mode, RaisedEvent, RT_Event } from "../statecharts/runtime_types"; -import { formatTime } from "../util/util"; -import { TimeMode, timeTravel } from "../statecharts/time"; -import { BigStepCause, TraceItem, TraceState } from "./App"; +import { Statechart, stateDescription } from "../../statecharts/abstract_syntax"; +import { Mode, RaisedEvent, RT_Event } from "../../statecharts/runtime_types"; +import { formatTime } from "../../util/util"; +import { TimeMode, timeTravel } from "../../statecharts/time"; import { Environment } from "@/statecharts/environment"; +import { BigStepCause, TraceItem, TraceState } from "../hooks/useSimulator"; type RTHistoryProps = { trace: TraceState|null, diff --git a/src/App/ShowAST.tsx b/src/App/SideBar/ShowAST.tsx similarity index 92% rename from src/App/ShowAST.tsx rename to src/App/SideBar/ShowAST.tsx index 8657331..6c240ad 100644 --- a/src/App/ShowAST.tsx +++ b/src/App/SideBar/ShowAST.tsx @@ -1,7 +1,9 @@ -import { ConcreteState, UnstableState, stateDescription, Transition } from "../statecharts/abstract_syntax"; -import { Action, EventTrigger, Expression } from "../statecharts/label_ast"; - -import "./AST.css"; +import BoltIcon from '@mui/icons-material/Bolt'; +import { memo, useEffect } from "react"; +import { usePersistentState } from "../../hooks/usePersistentState"; +import { ConcreteState, stateDescription, Transition, UnstableState } from "../../statecharts/abstract_syntax"; +import { Action, EventTrigger, Expression } from "../../statecharts/label_ast"; +import { KeyInfoHidden, KeyInfoVisible } from "../TopPanel/KeyInfo"; export function ShowTransition(props: {transition: Transition}) { return <>➝ {stateDescription(props.transition.tgt)}; @@ -46,10 +48,6 @@ export const ShowAST = memo(function ShowASTx(props: {root: ConcreteState | Unst ; }); -import BoltIcon from '@mui/icons-material/Bolt'; -import { KeyInfoHidden, KeyInfoVisible } from "./TopPanel/KeyInfo"; -import { memo, useEffect } from "react"; -import { usePersistentState } from "./persistent_state"; export function ShowInputEvents({inputEvents, onRaise, disabled, showKeys}: {inputEvents: EventTrigger[], onRaise: (e: string, p: any) => void, disabled: boolean, showKeys: boolean}) { const raiseHandlers = inputEvents.map(({event}) => { diff --git a/src/App/AST.css b/src/App/SideBar/SideBar.css similarity index 100% rename from src/App/AST.css rename to src/App/SideBar/SideBar.css diff --git a/src/App/SideBar.tsx b/src/App/SideBar/SideBar.tsx similarity index 98% rename from src/App/SideBar.tsx rename to src/App/SideBar/SideBar.tsx index ee2714b..27d42af 100644 --- a/src/App/SideBar.tsx +++ b/src/App/SideBar/SideBar.tsx @@ -8,14 +8,15 @@ import { Conns } from '@/statecharts/timed_reactive'; import { Dispatch, Ref, SetStateAction, useEffect, useRef, useState } from 'react'; import { Statechart } from '@/statecharts/abstract_syntax'; import { ShowAST, ShowInputEvents, ShowInternalEvents, ShowOutputEvents } from './ShowAST'; -import { Plant } from './Plant/Plant'; +import { Plant } from '../Plant/Plant'; import { checkProperty, PropertyCheckResult } from './check_property'; -import { Setters } from './makePartialSetter'; +import { Setters } from '../makePartialSetter'; import { RTHistory } from './RTHistory'; -import { BigStepCause, TraceState } from './useSimulator'; -import { plants, UniversalPlantState } from './plants'; +import { BigStepCause, TraceState } from '../hooks/useSimulator'; +import { plants, UniversalPlantState } from '../plants'; import { TimeMode } from '@/statecharts/time'; -import { PersistentDetails } from './PersistentDetails'; +import { PersistentDetails } from '../PersistentDetails'; +import "./SideBar.css"; type SavedTraces = [string, BigStepCause[]][]; diff --git a/src/App/check_property.ts b/src/App/SideBar/check_property.ts similarity index 96% rename from src/App/check_property.ts rename to src/App/SideBar/check_property.ts index 110112b..24a41cb 100644 --- a/src/App/check_property.ts +++ b/src/App/SideBar/check_property.ts @@ -1,6 +1,6 @@ import { RT_Statechart } from "@/statecharts/runtime_types"; -import { Plant } from "./Plant/Plant"; -import { TraceItem } from "./useSimulator"; +import { Plant } from "../Plant/Plant"; +import { TraceItem } from "../hooks/useSimulator"; // const endpoint = "http://localhost:15478/check_property"; const endpoint = "https://deemz.org/apis/mtl-aas/check_property"; diff --git a/src/App/TopPanel/TopPanel.tsx b/src/App/TopPanel/TopPanel.tsx index ed59b0a..a8272b2 100644 --- a/src/App/TopPanel/TopPanel.tsx +++ b/src/App/TopPanel/TopPanel.tsx @@ -18,10 +18,10 @@ import PlayArrowIcon from '@mui/icons-material/PlayArrow'; import SkipNextIcon from '@mui/icons-material/SkipNext'; import StopIcon from '@mui/icons-material/Stop'; import { InsertModes } from "./InsertModes"; -import { usePersistentState } from "@/App/persistent_state"; +import { usePersistentState } from "@/hooks/usePersistentState"; import { RotateButtons } from "./RotateButtons"; import { SpeedControl } from "./SpeedControl"; -import { TraceState } from "../useSimulator"; +import { TraceState } from "../hooks/useSimulator"; export type TopPanelProps = { trace: TraceState | null, diff --git a/src/App/VisualEditor/VisualEditor.tsx b/src/App/VisualEditor/VisualEditor.tsx index 9741520..8f2a191 100644 --- a/src/App/VisualEditor/VisualEditor.tsx +++ b/src/App/VisualEditor/VisualEditor.tsx @@ -1,21 +1,20 @@ -import { Dispatch, memo, ReactElement, SetStateAction, useCallback, useEffect, useRef, useState } from "react"; +import { Dispatch, memo, ReactElement, SetStateAction, useCallback, useEffect, useRef } from "react"; -import { InsertMode } from "../TopPanel/InsertModes"; import { Mode } from "@/statecharts/runtime_types"; import { arraysEqual, objectsEqual, setsEqual } from "@/util/util"; import { Arrow, ArrowPart, Diamond, History, RectSide, Rountangle, Text } from "../../statecharts/concrete_syntax"; import { Connections } from "../../statecharts/detect_connections"; import { TraceableError } from "../../statecharts/parser"; import { ArcDirection, arcDirection } from "../../util/geometry"; +import { InsertMode } from "../TopPanel/InsertModes"; import { ArrowSVG } from "./ArrowSVG"; import { DiamondSVG } from "./DiamondSVG"; import { HistorySVG } from "./HistorySVG"; import { RountangleSVG } from "./RountangleSVG"; import { TextSVG } from "./TextSVG"; -import { useCopyPaste } from "./useCopyPaste"; - import "./VisualEditor.css"; -import { useMouse } from "./useMouse"; +import { useCopyPaste } from "./hooks/useCopyPaste"; +import { useMouse } from "./hooks/useMouse"; export type ConcreteSyntax = { rountangles: Rountangle[]; diff --git a/src/App/VisualEditor/useCopyPaste.ts b/src/App/VisualEditor/hooks/useCopyPaste.ts similarity index 98% rename from src/App/VisualEditor/useCopyPaste.ts rename to src/App/VisualEditor/hooks/useCopyPaste.ts index e560cc0..21e8f9b 100644 --- a/src/App/VisualEditor/useCopyPaste.ts +++ b/src/App/VisualEditor/hooks/useCopyPaste.ts @@ -1,6 +1,6 @@ import { Arrow, Diamond, Rountangle, Text, History } from "@/statecharts/concrete_syntax"; import { ClipboardEvent, Dispatch, SetStateAction, useCallback, useEffect } from "react"; -import { Selection, VisualEditorState } from "./VisualEditor"; +import { Selection, VisualEditorState } from "../VisualEditor"; import { addV2D } from "@/util/geometry"; // const offset = {x: 40, y: 40}; diff --git a/src/App/VisualEditor/useMouse.tsx b/src/App/VisualEditor/hooks/useMouse.tsx similarity index 98% rename from src/App/VisualEditor/useMouse.tsx rename to src/App/VisualEditor/hooks/useMouse.tsx index 9ef94e5..1e917f7 100644 --- a/src/App/VisualEditor/useMouse.tsx +++ b/src/App/VisualEditor/hooks/useMouse.tsx @@ -2,10 +2,10 @@ import { rountangleMinSize } from "@/statecharts/concrete_syntax"; import { addV2D, area, isEntirelyWithin, normalizeRect, scaleV2D, subtractV2D, transformLine, transformRect } from "@/util/geometry"; import { getBBoxInSvgCoords } from "@/util/svg_helper"; import { Dispatch, useCallback, useEffect, useState } from "react"; -import { MIN_ROUNTANGLE_SIZE } from "../parameters"; -import { InsertMode } from "../TopPanel/InsertModes"; -import { Selecting, SelectingState } from "./Selection"; -import { Selection, VisualEditorState } from "./VisualEditor"; +import { MIN_ROUNTANGLE_SIZE } from "../../parameters"; +import { InsertMode } from "../../TopPanel/InsertModes"; +import { Selecting, SelectingState } from "../Selection"; +import { Selection, VisualEditorState } from "../VisualEditor"; export function useMouse(makeCheckPoint: () => void, insertMode: InsertMode, zoom: number, refSVG: {current: SVGSVGElement|null}, state: VisualEditorState, setState: Dispatch<(v: VisualEditorState) => VisualEditorState>, deleteSelection: () => void) { const [dragging, setDragging] = useState(false); diff --git a/src/App/useEditor.ts b/src/App/hooks/useEditor.ts similarity index 97% rename from src/App/useEditor.ts rename to src/App/hooks/useEditor.ts index e1ca293..058a5d5 100644 --- a/src/App/useEditor.ts +++ b/src/App/hooks/useEditor.ts @@ -1,8 +1,7 @@ import { addV2D, rotateLine90CCW, rotateLine90CW, rotatePoint90CCW, rotatePoint90CW, rotateRect90CCW, rotateRect90CW, scaleV2D, subtractV2D, Vec2D } from "@/util/geometry"; -import { HISTORY_RADIUS } from "./parameters"; +import { HISTORY_RADIUS } from "../parameters"; import { Dispatch, SetStateAction, useCallback, useEffect } from "react"; -import { EditHistory } from "./App"; -import { VisualEditorState } from "./VisualEditor/VisualEditor"; +import { EditHistory } from "../App"; export function useEditor(setEditHistory: Dispatch>) { useEffect(() => { diff --git a/src/App/useSimulator.ts b/src/App/hooks/useSimulator.ts similarity index 98% rename from src/App/useSimulator.ts rename to src/App/hooks/useSimulator.ts index 9844455..fa07dd0 100644 --- a/src/App/useSimulator.ts +++ b/src/App/hooks/useSimulator.ts @@ -3,9 +3,9 @@ import { RuntimeError } from "@/statecharts/interpreter"; import { BigStep, RaisedEvent } from "@/statecharts/runtime_types"; import { Conns, coupledExecution, statechartExecution } from "@/statecharts/timed_reactive"; import { useCallback, useEffect, useMemo, useState } from "react"; -import { Plant } from "./Plant/Plant"; +import { Plant } from "../Plant/Plant"; import { getSimTime, getWallClkDelay, TimeMode } from "@/statecharts/time"; -import { UniversalPlantState } from "./plants"; +import { UniversalPlantState } from "../plants"; type CoupledState = { sc: BigStep, @@ -107,6 +107,7 @@ export function useSimulator(ast: Statechart|null, plant: Plant { + // console.log('time effect:', time, currentTraceItem); let timeout: NodeJS.Timeout | undefined; if (currentTraceItem !== null && cE !== null) { if (currentTraceItem.kind === "bigstep") { diff --git a/src/App/makePartialSetter.ts b/src/App/makePartialSetter.ts index a9ef4a7..d3d5109 100644 --- a/src/App/makePartialSetter.ts +++ b/src/App/makePartialSetter.ts @@ -3,16 +3,16 @@ import { Dispatch, SetStateAction, useCallback, useMemo } from "react"; export function makePartialSetter(fullSetter: Dispatch>, key: K): Dispatch> { return (newValueOrCallback: T[K] | ((newValue: T[K]) => T[K])) => { fullSetter(oldFullValue => { - if (typeof newValueOrCallback === 'function') { + const newValue = (typeof newValueOrCallback === 'function') ? (newValueOrCallback as (newValue: T[K]) => T[K])(oldFullValue[key] as T[K]) : newValueOrCallback as T[K]; + if (newValue === oldFullValue[key]) { + return oldFullValue; + } + else { return { ...oldFullValue, - [key]: (newValueOrCallback as (newValue: T[K]) => T[K])(oldFullValue[key] as T[K]), + [key]: newValue, } } - return { - ...oldFullValue, - [key]: newValueOrCallback as T[K], - } }) }; } @@ -21,16 +21,16 @@ export type Setters = { [K in keyof T as `set${Capitalize>}`]: Dispatch>; } -export function makeIndividualSetters( +export function makeAllSetters( fullSetter: Dispatch>, keys: (keyof T)[], ): Setters { // @ts-ignore - return useMemo(() => + return useMemo(() => { + console.log('creating setters for App'); // @ts-ignore - Object.fromEntries(keys.map((key: string) => { + return Object.fromEntries(keys.map((key: string) => { return [`set${key.charAt(0).toUpperCase()}${key.slice(1)}`, makePartialSetter(fullSetter, key)]; - })), - [fullSetter] - ); + })); + }, [fullSetter]); } diff --git a/src/App/useAudioContext.ts b/src/hooks/useAudioContext.ts similarity index 100% rename from src/App/useAudioContext.ts rename to src/hooks/useAudioContext.ts diff --git a/src/hooks/useDetectChange.ts b/src/hooks/useDetectChange.ts new file mode 100644 index 0000000..3ef07c4 --- /dev/null +++ b/src/hooks/useDetectChange.ts @@ -0,0 +1,8 @@ +import { useEffect } from "react"; + +// useful for debugging +export function useDetectChange(expr: any, name: string) { + useEffect(() => { + console.log(name, 'changed to:', expr); + }, [expr]); +} diff --git a/src/App/persistent_state.ts b/src/hooks/usePersistentState.ts similarity index 100% rename from src/App/persistent_state.ts rename to src/hooks/usePersistentState.ts diff --git a/src/App/useUrlHashState.ts b/src/hooks/useUrlHashState.ts similarity index 83% rename from src/App/useUrlHashState.ts rename to src/hooks/useUrlHashState.ts index 3859d54..d442552 100644 --- a/src/App/useUrlHashState.ts +++ b/src/hooks/useUrlHashState.ts @@ -1,17 +1,17 @@ -import { useEffect } from "react"; +import { useEffect, useLayoutEffect } from "react"; // persist state in URL hash -export function useUrlHashState(recoverCallback: (recoveredState: T) => void): (toPersist: T) => void { +export function useUrlHashState(recoverCallback: (recoveredState: (T|null)) => void): (toPersist: T) => void { // recover editor state from URL - we need an effect here because decompression is asynchronous - useEffect(() => { + // layout effect because we want to run it before rendering the first frame + useLayoutEffect(() => { console.log('recovering state...'); const compressedState = window.location.hash.slice(1); if (compressedState.length === 0) { // empty URL hash console.log("no state to recover"); - // setEditHistory(() => ({current: emptyState, history: [], future: []})); - return; + return recoverCallback(null); } let compressedBuffer; try { @@ -19,8 +19,7 @@ export function useUrlHashState(recoverCallback: (recoveredState: T) => void) } catch (e) { // probably invalid base64 console.error("failed to recover state:", e); - // setEditHistory(() => ({current: emptyState, history: [], future: []})); - return; + return recoverCallback(null); } const ds = new DecompressionStream("deflate"); const writer = ds.writable.getWriter(); @@ -29,12 +28,13 @@ export function useUrlHashState(recoverCallback: (recoveredState: T) => void) new Response(ds.readable).arrayBuffer() .then(decompressedBuffer => { const recoveredState = JSON.parse(new TextDecoder().decode(decompressedBuffer)); + console.log('successfully recovered state'); recoverCallback(recoveredState); }) .catch(e => { // any other error: invalid JSON, or decompression failed. console.error("failed to recover state:", e); - // setEditHistory({current: emptyState, history: [], future: []}); + recoverCallback(null); }); }, []);