further reduce unnecessary re-renders

This commit is contained in:
Joeri Exelmans 2025-10-23 22:00:45 +02:00
parent 2ca2ba5d1b
commit 87ceaa1220
5 changed files with 40 additions and 26 deletions

View file

@ -18,9 +18,7 @@ import { formatTime } from "./util";
import { InsertMode } from "../VisualEditor/VisualEditor"; import { InsertMode } from "../VisualEditor/VisualEditor";
import { KeyInfoHidden, KeyInfoVisible } from "./KeyInfo"; import { KeyInfoHidden, KeyInfoVisible } from "./KeyInfo";
import { About } from "./About"; import { About } from "./About";
import { usePersistentState } from "@/util/persistent_state";
import { RountangleIcon, PseudoStateIcon, HistoryIcon } from "./Icons"; import { RountangleIcon, PseudoStateIcon, HistoryIcon } from "./Icons";
import { ZOOM_MAX, ZOOM_MIN, ZOOM_STEP } from "@/VisualEditor/parameters";
import { EditHistory, TraceState } from "./App"; import { EditHistory, TraceState } from "./App";
import { ZoomButtons } from "./TopPanel/ZoomButtons"; import { ZoomButtons } from "./TopPanel/ZoomButtons";
import { UndoRedoButtons } from "./TopPanel/UndoRedoButtons"; import { UndoRedoButtons } from "./TopPanel/UndoRedoButtons";
@ -46,6 +44,18 @@ export type TopPanelProps = {
history: EditHistory, history: EditHistory,
} }
const ShortCutShowKeys = <kbd>~</kbd>;
const insertModes: [InsertMode, string, ReactElement, ReactElement][] = [
["and", "AND-states", <RountangleIcon kind="and"/>, <kbd>A</kbd>],
["or", "OR-states", <RountangleIcon kind="or"/>, <kbd>O</kbd>],
["pseudo", "pseudo-states", <PseudoStateIcon/>, <kbd>P</kbd>],
["shallow", "shallow history", <HistoryIcon kind="shallow"/>, <kbd>H</kbd>],
["deep", "deep history", <HistoryIcon kind="deep"/>, <></>],
["transition", "transitions", <TrendingFlatIcon fontSize="small"/>, <kbd>T</kbd>],
["text", "text", <>&nbsp;T&nbsp;</>, <kbd>X</kbd>],
];
export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, onRedo, onInit, onClear, onBack, mode, setMode, setModal, zoom, setZoom, showKeys, setShowKeys, history}: TopPanelProps) { export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, onRedo, onInit, onClear, onBack, mode, setMode, setModal, zoom, setZoom, showKeys, setShowKeys, history}: TopPanelProps) {
const [displayTime, setDisplayTime] = useState("0.000"); const [displayTime, setDisplayTime] = useState("0.000");
const [timescale, setTimescale] = useState(1); const [timescale, setTimescale] = useState(1);
@ -191,7 +201,7 @@ export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, on
{/* shortcuts / about */} {/* shortcuts / about */}
<div className="toolbarGroup"> <div className="toolbarGroup">
<KeyInfo keyInfo={<kbd>~</kbd>}> <KeyInfo keyInfo={ShortCutShowKeys}>
<button title="show/hide keyboard shortcuts" className={showKeys?"active":""} onClick={() => setShowKeys(s => !s)}><KeyboardIcon fontSize="small"/></button> <button title="show/hide keyboard shortcuts" className={showKeys?"active":""} onClick={() => setShowKeys(s => !s)}><KeyboardIcon fontSize="small"/></button>
</KeyInfo> </KeyInfo>
<button title="about StateBuddy" onClick={() => setModal(<About setModal={setModal}/>)}><InfoOutlineIcon fontSize="small"/></button> <button title="about StateBuddy" onClick={() => setModal(<About setModal={setModal}/>)}><InfoOutlineIcon fontSize="small"/></button>
@ -212,15 +222,7 @@ export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, on
{/* insert rountangle / arrow / ... */} {/* insert rountangle / arrow / ... */}
<div className="toolbarGroup"> <div className="toolbarGroup">
{([ {insertModes.map(([m, hint, buttonTxt, keyInfo]) =>
["and", "AND-states", <RountangleIcon kind="and"/>, <kbd>A</kbd>],
["or", "OR-states", <RountangleIcon kind="or"/>, <kbd>O</kbd>],
["pseudo", "pseudo-states", <PseudoStateIcon/>, <kbd>P</kbd>],
["shallow", "shallow history", <HistoryIcon kind="shallow"/>, <kbd>H</kbd>],
["deep", "deep history", <HistoryIcon kind="deep"/>, <></>],
["transition", "transitions", <TrendingFlatIcon fontSize="small"/>, <kbd>T</kbd>],
["text", "text", <>&nbsp;T&nbsp;</>, <kbd>X</kbd>],
] as [InsertMode, string, ReactElement, ReactElement][]).map(([m, hint, buttonTxt, keyInfo]) =>
<KeyInfo key={m} keyInfo={keyInfo}> <KeyInfo key={m} keyInfo={keyInfo}>
<button <button
title={"insert "+hint} title={"insert "+hint}

View file

@ -5,6 +5,9 @@ import { KeyInfoHidden, KeyInfoVisible } from "../KeyInfo";
import ZoomInIcon from '@mui/icons-material/ZoomIn'; import ZoomInIcon from '@mui/icons-material/ZoomIn';
import ZoomOutIcon from '@mui/icons-material/ZoomOut'; import ZoomOutIcon from '@mui/icons-material/ZoomOut';
const shortcutZoomIn = <><kbd>Ctrl</kbd>+<kbd>-</kbd></>;
const shortcutZoomOut = <><kbd>Ctrl</kbd>+<kbd>+</kbd></>;
export const ZoomButtons = memo(function ZoomButtons({showKeys, zoom, setZoom}: {showKeys: boolean, zoom: number, setZoom: Dispatch<SetStateAction<number>>}) { export const ZoomButtons = memo(function ZoomButtons({showKeys, zoom, setZoom}: {showKeys: boolean, zoom: number, setZoom: Dispatch<SetStateAction<number>>}) {
const KeyInfo = showKeys ? KeyInfoVisible : KeyInfoHidden; const KeyInfo = showKeys ? KeyInfoVisible : KeyInfoHidden;
@ -36,11 +39,11 @@ export const ZoomButtons = memo(function ZoomButtons({showKeys, zoom, setZoom}:
}, []); }, []);
return <> return <>
<KeyInfo keyInfo={<><kbd>Ctrl</kbd>+<kbd>-</kbd></>}> <KeyInfo keyInfo={shortcutZoomOut}>
<button title="zoom out" onClick={onZoomOut} disabled={zoom <= ZOOM_MIN}><ZoomOutIcon fontSize="small"/></button> <button title="zoom out" onClick={onZoomOut} disabled={zoom <= ZOOM_MIN}><ZoomOutIcon fontSize="small"/></button>
</KeyInfo> </KeyInfo>
<input title="current zoom level" value={zoom.toFixed(3)} style={{width:40}} readOnly/> <input title="current zoom level" value={zoom.toFixed(3)} style={{width:40}} readOnly/>
<KeyInfo keyInfo={<><kbd>Ctrl</kbd>+<kbd>+</kbd></>}> <KeyInfo keyInfo={shortcutZoomIn}>
<button title="zoom in" onClick={onZoomIn} disabled={zoom >= ZOOM_MAX}><ZoomInIcon fontSize="small"/></button> <button title="zoom in" onClick={onZoomIn} disabled={zoom >= ZOOM_MAX}><ZoomInIcon fontSize="small"/></button>
</KeyInfo> </KeyInfo>
</>; </>;

View file

@ -1,10 +1,11 @@
import { memo } from "react"; import { memo } from "react";
import { Arrow } from "../statecharts/concrete_syntax"; import { Arrow, ArrowPart } from "../statecharts/concrete_syntax";
import { ArcDirection, euclideanDistance } from "./geometry"; import { ArcDirection, euclideanDistance } from "./geometry";
import { CORNER_HELPER_RADIUS } from "./parameters"; import { CORNER_HELPER_RADIUS } from "./parameters";
import { arraysEqual } from "@/App/util";
export const ArrowSVG = memo(function(props: { arrow: Arrow; selected: string[]; errors: string[]; highlight: boolean; fired: boolean; arc: ArcDirection; initialMarker: boolean }) { export const ArrowSVG = memo(function(props: { arrow: Arrow; selected: ArrowPart[]; error: string; highlight: boolean; fired: boolean; arc: ArcDirection; initialMarker: boolean }) {
const { start, end, uid } = props.arrow; const { start, end, uid } = props.arrow;
const radius = euclideanDistance(start, end) / 1.6; const radius = euclideanDistance(start, end) / 1.6;
let largeArc = "1"; let largeArc = "1";
@ -18,7 +19,7 @@ export const ArrowSVG = memo(function(props: { arrow: Arrow; selected: string[];
<path <path
className={"arrow" className={"arrow"
+ (props.selected.length === 2 ? " selected" : "") + (props.selected.length === 2 ? " selected" : "")
+ (props.errors.length > 0 ? " error" : "") + (props.error ? " error" : "")
+ (props.highlight ? " highlight" : "") + (props.highlight ? " highlight" : "")
+ (props.fired ? " fired" : "") + (props.fired ? " fired" : "")
} }
@ -30,13 +31,13 @@ export const ArrowSVG = memo(function(props: { arrow: Arrow; selected: string[];
data-uid={uid} data-uid={uid}
data-parts="start end" /> data-parts="start end" />
{props.errors.length > 0 && <text {props.error && <text
className="error" className="error"
x={(start.x + end.x) / 2 + 5} x={(start.x + end.x) / 2 + 5}
y={(start.y + end.y) / 2} y={(start.y + end.y) / 2}
textAnchor="middle" textAnchor="middle"
data-uid={uid} data-uid={uid}
data-parts="start end">{props.errors.join(', ')}</text>} data-parts="start end">{props.error}</text>}
<path <path
className="helper" className="helper"
@ -79,4 +80,12 @@ export const ArrowSVG = memo(function(props: { arrow: Arrow; selected: string[];
data-parts="end" />} data-parts="end" />}
</g>; </g>;
}); }, (prevProps, nextProps) => {
return prevProps.arrow === nextProps.arrow
&& arraysEqual(prevProps.selected, nextProps.selected)
&& prevProps.highlight === nextProps.highlight
&& prevProps.error === nextProps.error
&& prevProps.fired === nextProps.fired
&& prevProps.arc === nextProps.arc
&& prevProps.initialMarker === nextProps.initialMarker
})

View file

@ -755,10 +755,10 @@ export const VisualEditor = memo(function VisualEditor({state, setState, setAST,
return <ArrowSVG return <ArrowSVG
key={arrow.uid} key={arrow.uid}
arrow={arrow} arrow={arrow}
selected={selection.find(a => a.uid === arrow.uid)?.parts || []} selected={selection.find(a => a.uid === arrow.uid)?.parts as ArrowPart[] || []}
errors={errors error={errors
.filter(({shapeUid}) => shapeUid === arrow.uid) .filter(({shapeUid}) => shapeUid === arrow.uid)
.map(({message}) => message)} .map(({message}) => message).join(', ')}
highlight={arrowsToHighlight.hasOwnProperty(arrow.uid)} highlight={arrowsToHighlight.hasOwnProperty(arrow.uid)}
fired={highlightTransitions.includes(arrow.uid)} fired={highlightTransitions.includes(arrow.uid)}
arc={arc} arc={arc}

View file

@ -1,4 +1,4 @@
import { Dispatch, SetStateAction, useState } from "react"; import { Dispatch, SetStateAction, useCallback, useState } from "react";
// like useState, but it is persisted in localStorage // like useState, but it is persisted in localStorage
// important: values must be JSON-(de-)serializable // important: values must be JSON-(de-)serializable
@ -18,7 +18,7 @@ export function usePersistentState<T>(key: string, initial: T): [T, Dispatch<Set
return initial; return initial;
}); });
function setStateWrapped(val: SetStateAction<T>) { const setStateWrapped = useCallback((val: SetStateAction<T>) => {
setState((oldState: T) => { setState((oldState: T) => {
let newVal; let newVal;
if (typeof val === 'function') { if (typeof val === 'function') {
@ -32,7 +32,7 @@ export function usePersistentState<T>(key: string, initial: T): [T, Dispatch<Set
localStorage.setItem(key, serialized); localStorage.setItem(key, serialized);
return newVal; return newVal;
}); });
} }, [setState]);
return [state, setStateWrapped]; return [state, setStateWrapped];
} }