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 { KeyInfoHidden, KeyInfoVisible } from "./KeyInfo";
import { About } from "./About";
import { usePersistentState } from "@/util/persistent_state";
import { RountangleIcon, PseudoStateIcon, HistoryIcon } from "./Icons";
import { ZOOM_MAX, ZOOM_MIN, ZOOM_STEP } from "@/VisualEditor/parameters";
import { EditHistory, TraceState } from "./App";
import { ZoomButtons } from "./TopPanel/ZoomButtons";
import { UndoRedoButtons } from "./TopPanel/UndoRedoButtons";
@ -46,6 +44,18 @@ export type TopPanelProps = {
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) {
const [displayTime, setDisplayTime] = useState("0.000");
const [timescale, setTimescale] = useState(1);
@ -191,7 +201,7 @@ export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, on
{/* shortcuts / about */}
<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>
</KeyInfo>
<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 / ... */}
<div className="toolbarGroup">
{([
["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]) =>
{insertModes.map(([m, hint, buttonTxt, keyInfo]) =>
<KeyInfo key={m} keyInfo={keyInfo}>
<button
title={"insert "+hint}

View file

@ -5,6 +5,9 @@ import { KeyInfoHidden, KeyInfoVisible } from "../KeyInfo";
import ZoomInIcon from '@mui/icons-material/ZoomIn';
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>>}) {
const KeyInfo = showKeys ? KeyInfoVisible : KeyInfoHidden;
@ -36,11 +39,11 @@ export const ZoomButtons = memo(function ZoomButtons({showKeys, zoom, setZoom}:
}, []);
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>
</KeyInfo>
<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>
</KeyInfo>
</>;

View file

@ -1,10 +1,11 @@
import { memo } from "react";
import { Arrow } from "../statecharts/concrete_syntax";
import { Arrow, ArrowPart } from "../statecharts/concrete_syntax";
import { ArcDirection, euclideanDistance } from "./geometry";
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 radius = euclideanDistance(start, end) / 1.6;
let largeArc = "1";
@ -18,7 +19,7 @@ export const ArrowSVG = memo(function(props: { arrow: Arrow; selected: string[];
<path
className={"arrow"
+ (props.selected.length === 2 ? " selected" : "")
+ (props.errors.length > 0 ? " error" : "")
+ (props.error ? " error" : "")
+ (props.highlight ? " highlight" : "")
+ (props.fired ? " fired" : "")
}
@ -30,13 +31,13 @@ export const ArrowSVG = memo(function(props: { arrow: Arrow; selected: string[];
data-uid={uid}
data-parts="start end" />
{props.errors.length > 0 && <text
{props.error && <text
className="error"
x={(start.x + end.x) / 2 + 5}
y={(start.y + end.y) / 2}
textAnchor="middle"
data-uid={uid}
data-parts="start end">{props.errors.join(', ')}</text>}
data-parts="start end">{props.error}</text>}
<path
className="helper"
@ -79,4 +80,12 @@ export const ArrowSVG = memo(function(props: { arrow: Arrow; selected: string[];
data-parts="end" />}
</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
key={arrow.uid}
arrow={arrow}
selected={selection.find(a => a.uid === arrow.uid)?.parts || []}
errors={errors
selected={selection.find(a => a.uid === arrow.uid)?.parts as ArrowPart[] || []}
error={errors
.filter(({shapeUid}) => shapeUid === arrow.uid)
.map(({message}) => message)}
.map(({message}) => message).join(', ')}
highlight={arrowsToHighlight.hasOwnProperty(arrow.uid)}
fired={highlightTransitions.includes(arrow.uid)}
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
// important: values must be JSON-(de-)serializable
@ -18,7 +18,7 @@ export function usePersistentState<T>(key: string, initial: T): [T, Dispatch<Set
return initial;
});
function setStateWrapped(val: SetStateAction<T>) {
const setStateWrapped = useCallback((val: SetStateAction<T>) => {
setState((oldState: T) => {
let newVal;
if (typeof val === 'function') {
@ -32,7 +32,7 @@ export function usePersistentState<T>(key: string, initial: T): [T, Dispatch<Set
localStorage.setItem(key, serialized);
return newVal;
});
}
}, [setState]);
return [state, setStateWrapped];
}