diff --git a/src/App.tsx b/src/App.tsx index 62c4cd4..144a563 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -31,6 +31,7 @@ import type { ExportOptions } from './utils/exportImport'; import { useAppState } from './hooks/useAppState'; import { useSimulation } from './hooks/useSimulation'; import { useLanguage } from './hooks/useLanguage'; +import { useWindowSize } from './hooks/useWindowSize'; // --- Main Component --- const MedPlanAssistant = () => { @@ -59,17 +60,9 @@ const MedPlanAssistant = () => { }; // Use shorter button labels on narrow screens to keep the pin control visible - const [useCompactButtons, setUseCompactButtons] = React.useState(false); - - React.useEffect(() => { - const updateCompact = () => { - setUseCompactButtons(window.innerWidth < 520); // tweakable threshold - }; - - updateCompact(); - window.addEventListener('resize', updateCompact); - return () => window.removeEventListener('resize', updateCompact); - }, []); + // Using debounced window size to prevent performance issues during resize + const { width: windowWidth } = useWindowSize(150); + const useCompactButtons = windowWidth < 520; // tweakable threshold const { appState, diff --git a/src/components/settings.tsx b/src/components/settings.tsx index 829f4a7..07b567d 100644 --- a/src/components/settings.tsx +++ b/src/components/settings.tsx @@ -10,6 +10,7 @@ */ import React from 'react'; +import { useWindowSize } from '../hooks/useWindowSize'; import { Card, CardContent } from './ui/card'; import { Label } from './ui/label'; import { Switch } from './ui/switch'; @@ -108,20 +109,9 @@ const Settings = ({ const [therapeuticRangeError, setTherapeuticRangeError] = React.useState(''); const [yAxisRangeError, setYAxisRangeError] = React.useState(''); - // Track window width for responsive tooltip positioning - const [isNarrowScreen, setIsNarrowScreen] = React.useState( - typeof window !== 'undefined' ? window.innerWidth < 640 : false - ); - - // Update narrow screen state on window resize - React.useEffect(() => { - const handleResize = () => { - setIsNarrowScreen(window.innerWidth < 640); - }; - - window.addEventListener('resize', handleResize); - return () => window.removeEventListener('resize', handleResize); - }, []); + // Track window width for responsive tooltip positioning using debounced hook + const { width: windowWidth } = useWindowSize(150); + const isNarrowScreen = windowWidth < 640; // Determine tooltip side based on screen width const tooltipSide = isNarrowScreen ? 'top' : 'right'; diff --git a/src/components/simulation-chart.tsx b/src/components/simulation-chart.tsx index 130a808..c2f15ef 100644 --- a/src/components/simulation-chart.tsx +++ b/src/components/simulation-chart.tsx @@ -26,6 +26,7 @@ import { TooltipTrigger as UiTooltipTrigger, TooltipContent as UiTooltipContent, } from './ui/tooltip'; +import { useElementSize } from '../hooks/useElementSize'; // Chart color scheme const CHART_COLORS = { @@ -70,21 +71,9 @@ const SimulationChart = ({ const dispDays = parseInt(displayedDays, 10) || 2; const simDays = parseInt(simulationDays, 10) || 3; - // Calculate chart dimensions - const [containerWidth, setContainerWidth] = React.useState(1000); + // Calculate chart dimensions using debounced element size observer const containerRef = React.useRef(null); - - React.useEffect(() => { - const updateWidth = () => { - if (containerRef.current) { - setContainerWidth(containerRef.current.clientWidth); - } - }; - - updateWidth(); - window.addEventListener('resize', updateWidth); - return () => window.removeEventListener('resize', updateWidth); - }, []); + const { width: containerWidth } = useElementSize(containerRef, 150); // Track current theme for chart styling const [isDarkTheme, setIsDarkTheme] = React.useState(false); diff --git a/src/hooks/useDebounce.ts b/src/hooks/useDebounce.ts new file mode 100644 index 0000000..f69fdcc --- /dev/null +++ b/src/hooks/useDebounce.ts @@ -0,0 +1,33 @@ +/** + * useDebounce Hook + * + * Debounces a value to prevent excessive updates. + * Useful for performance optimization with frequently changing values. + * + * @author Andreas Weyer + * @license MIT + */ + +import { useEffect, useState } from 'react'; + +/** + * Debounces a value by delaying its update + * @param value - The value to debounce + * @param delay - Delay in milliseconds (default: 150ms) + * @returns The debounced value + */ +export function useDebounce(value: T, delay: number = 150): T { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => { + clearTimeout(handler); + }; + }, [value, delay]); + + return debouncedValue; +} diff --git a/src/hooks/useElementSize.ts b/src/hooks/useElementSize.ts new file mode 100644 index 0000000..7ba656f --- /dev/null +++ b/src/hooks/useElementSize.ts @@ -0,0 +1,63 @@ +/** + * useElementSize Hook + * + * Tracks element dimensions using ResizeObserver with debouncing. + * More efficient than window resize events for container-specific sizing. + * + * @author Andreas Weyer + * @license MIT + */ + +import { useEffect, useState, RefObject } from 'react'; +import { useDebounce } from './useDebounce'; + +interface ElementSize { + width: number; + height: number; +} + +/** + * Hook to track element size with debouncing + * @param ref - React ref to the element to observe + * @param debounceDelay - Delay in milliseconds for debouncing (default: 150ms) + * @returns Current element dimensions (debounced) + */ +export function useElementSize( + ref: RefObject, + debounceDelay: number = 150 +): ElementSize { + const [size, setSize] = useState({ + width: 1000, + height: 600, + }); + + // Debounce the size to prevent excessive re-renders + const debouncedSize = useDebounce(size, debounceDelay); + + useEffect(() => { + const element = ref.current; + if (!element) return; + + // Set initial size + setSize({ + width: element.clientWidth, + height: element.clientHeight, + }); + + // Use ResizeObserver for efficient element size tracking + const resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + const { width, height } = entry.contentRect; + setSize({ width, height }); + } + }); + + resizeObserver.observe(element); + + return () => { + resizeObserver.disconnect(); + }; + }, [ref]); + + return debouncedSize; +} diff --git a/src/hooks/useWindowSize.ts b/src/hooks/useWindowSize.ts new file mode 100644 index 0000000..4f97a29 --- /dev/null +++ b/src/hooks/useWindowSize.ts @@ -0,0 +1,46 @@ +/** + * useWindowSize Hook + * + * Tracks window dimensions with debouncing to prevent excessive re-renders + * during window resize operations. + * + * @author Andreas Weyer + * @license MIT + */ + +import { useEffect, useState } from 'react'; +import { useDebounce } from './useDebounce'; + +interface WindowSize { + width: number; + height: number; +} + +/** + * Hook to track window size with debouncing + * @param debounceDelay - Delay in milliseconds for debouncing (default: 150ms) + * @returns Current window dimensions (debounced) + */ +export function useWindowSize(debounceDelay: number = 150): WindowSize { + const [windowSize, setWindowSize] = useState({ + width: typeof window !== 'undefined' ? window.innerWidth : 1000, + height: typeof window !== 'undefined' ? window.innerHeight : 800, + }); + + // Debounce the window size to prevent excessive re-renders + const debouncedWindowSize = useDebounce(windowSize, debounceDelay); + + useEffect(() => { + const handleResize = () => { + setWindowSize({ + width: window.innerWidth, + height: window.innerHeight, + }); + }; + + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + + return debouncedWindowSize; +}