diff --git a/biome.json b/biome.json index dd696cec..8c251af0 100644 --- a/biome.json +++ b/biome.json @@ -46,6 +46,16 @@ "noUnusedVariables": { "level": "warn", "fix": "unsafe" + }, + "useExhaustiveDependencies": { + "level": "error", + "options": { + "hooks": [ + { "name": "useLazyRef", "stableResult": true }, + { "name": "useSignal", "stableResult": true }, + { "name": "useComputed", "stableResult": true } + ] + } } }, "suspicious": { diff --git a/packages/scan/src/web/components/copy-to-clipboard/index.tsx b/packages/scan/src/web/components/copy-to-clipboard/index.tsx index 7cac903f..8fe952ff 100644 --- a/packages/scan/src/web/components/copy-to-clipboard/index.tsx +++ b/packages/scan/src/web/components/copy-to-clipboard/index.tsx @@ -1,5 +1,6 @@ +import { useSignal, useSignalEffect } from '@preact/signals'; import { memo } from 'preact/compat'; -import { useCallback, useEffect, useState } from 'preact/hooks'; +import { useCallback } from 'preact/hooks'; import { cn } from '~web/utils/helpers'; import { Icon } from '../icon'; @@ -22,16 +23,18 @@ export const CopyToClipboard = memo( className, iconSize = 14, }: CopyToClipboardProps): JSX.Element => { - const [isCopied, setIsCopied] = useState(false); + const isCopied = useSignal(false); - useEffect(() => { - if (isCopied) { - const timeout = setTimeout(() => setIsCopied(false), 600); + useSignalEffect(() => { + if (isCopied.value) { + const timeout = setTimeout(() => { + isCopied.value = false; + }, 600); return () => { clearTimeout(timeout); }; } - }, [isCopied]); + }); const copyToClipboard = useCallback( (e: MouseEvent) => { @@ -40,7 +43,7 @@ export const CopyToClipboard = memo( navigator.clipboard.writeText(text).then( () => { - setIsCopied(true); + isCopied.value = true; onCopy?.(true, text); }, () => { @@ -66,9 +69,9 @@ export const CopyToClipboard = memo( )} > ); diff --git a/packages/scan/src/web/hooks/use-merged-refs.ts b/packages/scan/src/web/hooks/use-merged-refs.ts index ae5853da..638cb1e2 100644 --- a/packages/scan/src/web/hooks/use-merged-refs.ts +++ b/packages/scan/src/web/hooks/use-merged-refs.ts @@ -1,5 +1,6 @@ import type { Ref, RefCallback } from 'preact'; -import { type MutableRefObject, useCallback } from 'preact/compat'; +import type { MutableRefObject } from 'preact/compat'; +import { useMemo } from 'react'; type PossibleRef = Ref | undefined; @@ -11,16 +12,20 @@ const assignRef = (ref: PossibleRef, value: T) => { } }; -const mergeRefs = (...refs: PossibleRef[]) => { - return (node: T) => { - for (const ref of refs) { +function assignRefs(this: PossibleRef[], node: T | null) { + if (node) { + for (const ref of this) { if (ref) { assignRef(ref, node); } } - }; -}; + } +} + +function mergeRefs(this: PossibleRef[]): RefCallback { + return (assignRefs).bind(this); +} -export const useMergedRefs = (...refs: PossibleRef[]) => { - return useCallback(mergeRefs(...refs), [...refs]) as RefCallback; +export const useMergedRefs = (...refs: PossibleRef[]): RefCallback => { + return useMemo((mergeRefs).bind(refs), refs); }; diff --git a/packages/scan/src/web/views/index.tsx b/packages/scan/src/web/views/index.tsx index 370d88af..9cd69c6a 100644 --- a/packages/scan/src/web/views/index.tsx +++ b/packages/scan/src/web/views/index.tsx @@ -84,15 +84,13 @@ interface ContentViewProps { const ContentView = ({ isOpen, children }: ContentViewProps) => { return (
- cn( - 'flex-1', - 'opacity-0', - 'overflow-y-auto overflow-x-hidden', - 'transition-opacity delay-0', - 'pointer-events-none', - isOpen.value && 'opacity-100 delay-150 pointer-events-auto', - ), + className={cn( + 'flex-1', + 'opacity-0', + 'overflow-y-auto overflow-x-hidden', + 'transition-opacity delay-0', + 'pointer-events-none', + isOpen.value && 'opacity-100 delay-150 pointer-events-auto', )} >
{children}
diff --git a/packages/scan/src/web/views/inspector/components-tree/index.tsx b/packages/scan/src/web/views/inspector/components-tree/index.tsx index ba0b01c1..eadb663d 100644 --- a/packages/scan/src/web/views/inspector/components-tree/index.tsx +++ b/packages/scan/src/web/views/inspector/components-tree/index.tsx @@ -21,6 +21,7 @@ import { saveLocalStorage, } from '~web/utils/helpers'; import { getFiberPath } from '~web/utils/pin'; +import { createSet } from '../factories'; import { inspectorUpdateSignal } from '../states'; import { type InspectableElement, @@ -396,7 +397,7 @@ export const ComponentsTree = () => { const refResizeHandle = useRef(null); const [flattenedNodes, setFlattenedNodes] = useState([]); - const [collapsedNodes, setCollapsedNodes] = useState>(new Set()); + const [collapsedNodes, setCollapsedNodes] = useState(createSet); const [selectedIndex, setSelectedIndex] = useState( undefined, ); diff --git a/packages/scan/src/web/views/inspector/diff-value.tsx b/packages/scan/src/web/views/inspector/diff-value.tsx index 3298a7ab..bdac8425 100644 --- a/packages/scan/src/web/views/inspector/diff-value.tsx +++ b/packages/scan/src/web/views/inspector/diff-value.tsx @@ -45,9 +45,8 @@ const TreeNode = ({ isNegative: boolean; }) => { const [isExpanded, setIsExpanded] = useState(false); - const canExpand = value !== null && - typeof value === 'object' && - !(value instanceof Date); + const canExpand = + value !== null && typeof value === 'object' && !(value instanceof Date); if (!canExpand) { return ( @@ -81,7 +80,9 @@ const TreeNode = ({ {path}: {!isExpanded && ( - {value instanceof Date ? formatValuePreview(value) : `{${Object.keys(value).join(', ')}}`} + {value instanceof Date + ? formatValuePreview(value) + : `{${Object.keys(value).join(', ')}}`} )}
@@ -180,16 +181,16 @@ export const DiffValueView = ({ {!expanded ? ( {formatValuePreview(safeValue)} ) : ( -
- {Object.entries(safeValue as object).map(([key, val]) => ( - - ))} -
+
+ {Object.entries(safeValue as object).map(([key, val]) => ( + + ))} +
)} (): Set { + return new Set(); +} + +export function createMap(): Map { + return new Map(); +} diff --git a/packages/scan/src/web/views/inspector/overlay/index.tsx b/packages/scan/src/web/views/inspector/overlay/index.tsx index 47af8f25..8c116a9d 100644 --- a/packages/scan/src/web/views/inspector/overlay/index.tsx +++ b/packages/scan/src/web/views/inspector/overlay/index.tsx @@ -43,6 +43,43 @@ export const OVERLAY_DPR = export const currentLockIconRect: LockIconRect | null = null; + +const drawLockIcon = ( + ctx: CanvasRenderingContext2D, + x: number, + y: number, + size: number, +) => { + ctx.save(); + ctx.strokeStyle = 'white'; + ctx.fillStyle = 'white'; + ctx.lineWidth = 1.5; + + const shackleWidth = size * 0.6; + const shackleHeight = size * 0.5; + const shackleX = x + (size - shackleWidth) / 2; + const shackleY = y; + + ctx.beginPath(); + ctx.arc( + shackleX + shackleWidth / 2, + shackleY + shackleHeight / 2, + shackleWidth / 2, + Math.PI, + 0, + false, + ); + ctx.stroke(); + + const bodyWidth = size * 0.8; + const bodyHeight = size * 0.5; + const bodyX = x + (size - bodyWidth) / 2; + const bodyY = y + shackleHeight / 2; + + ctx.fillRect(bodyX, bodyY, bodyWidth, bodyHeight); + ctx.restore(); +}; + export const ScanOverlay = () => { const refCanvas = useRef(null); const refEventCatcher = useRef(null); @@ -57,42 +94,6 @@ export const ScanOverlay = () => { const refIsFadingOut = useRef(false); const refLastFrameTime = useRef(0); - const drawLockIcon = ( - ctx: CanvasRenderingContext2D, - x: number, - y: number, - size: number, - ) => { - ctx.save(); - ctx.strokeStyle = 'white'; - ctx.fillStyle = 'white'; - ctx.lineWidth = 1.5; - - const shackleWidth = size * 0.6; - const shackleHeight = size * 0.5; - const shackleX = x + (size - shackleWidth) / 2; - const shackleY = y; - - ctx.beginPath(); - ctx.arc( - shackleX + shackleWidth / 2, - shackleY + shackleHeight / 2, - shackleWidth / 2, - Math.PI, - 0, - false, - ); - ctx.stroke(); - - const bodyWidth = size * 0.8; - const bodyHeight = size * 0.5; - const bodyX = x + (size - bodyWidth) / 2; - const bodyY = y + shackleHeight / 2; - - ctx.fillRect(bodyX, bodyY, bodyWidth, bodyHeight); - ctx.restore(); - }; - const drawStatsPill = ( ctx: CanvasRenderingContext2D, rect: Rect, diff --git a/packages/scan/src/web/views/inspector/timeline/index.tsx b/packages/scan/src/web/views/inspector/timeline/index.tsx index f36d2d88..e51f94df 100644 --- a/packages/scan/src/web/views/inspector/timeline/index.tsx +++ b/packages/scan/src/web/views/inspector/timeline/index.tsx @@ -1,44 +1,40 @@ +import { computed, useComputed } from '@preact/signals'; import { isInstrumentationActive } from 'bippy'; import { memo } from 'preact/compat'; -import { useCallback, useEffect, useMemo, useRef } from 'preact/hooks'; +import { useCallback } from 'preact/hooks'; import { Icon } from '~web/components/icon'; import { Slider } from '~web/components/slider'; import type { useMergedRefs } from '~web/hooks/use-merged-refs'; -import { - timelineActions, - timelineState, -} from '../states'; +import { timelineActions, timelineState } from '../states'; import { calculateSliderValues } from '../utils'; interface TimelineProps { refSticky?: - | ReturnType> - | ((node: HTMLElement | null) => void); + | ReturnType> + | ((node: HTMLElement | null) => void); } -export const Timeline = memo(({ - refSticky, -}: TimelineProps) => { - const refPlayInterval = useRef(null); +const buttonTitle = computed(() => + timelineState.value.isVisible + ? 'Hide Re-renders History' + : 'View Re-renders History', +); - const { - currentIndex, - isVisible, - totalUpdates, - updates, - } = timelineState.value; - - const sliderValues = useMemo(() => { - return calculateSliderValues(totalUpdates, currentIndex); - }, [totalUpdates, currentIndex]); +export const Timeline = memo(({ refSticky }: TimelineProps) => { + const sliderValues = useComputed(() => { + return calculateSliderValues( + timelineState.value.totalUpdates, + timelineState.value.currentIndex, + ); + }); const handleSliderChange = async (e: Event) => { + const { updates } = timelineState.value; const target = e.target as HTMLInputElement; const value = Number.parseInt(target.value, 10); const newIndex = Math.min(updates.length - 1, Math.max(0, value)); - let isViewingHistory = false; if (newIndex > 0 && newIndex < updates.length - 1) { isViewingHistory = true; @@ -46,76 +42,66 @@ export const Timeline = memo(({ timelineActions.updateFrame(newIndex, isViewingHistory); }; - useEffect(() => { - return () => { - if (refPlayInterval.current) { - clearInterval(refPlayInterval.current); - } - }; - }, []); - - const handleShowTimeline = useCallback(() => { - if (!isVisible) { + const handleShowTimeline = () => { + if (!timelineState.value.isVisible) { timelineActions.showTimeline(); } - }, [isVisible]); + }; const handleHideTimeline = useCallback((e: Event) => { - e.preventDefault(); - e.stopPropagation(); - if (refPlayInterval.current) { - clearInterval(refPlayInterval.current); - refPlayInterval.current = null; + if (timelineState.value.isVisible) { + e.preventDefault(); + e.stopPropagation(); + timelineActions.hideTimeline(); } - timelineActions.hideTimeline(); }, []); - if (!isInstrumentationActive()) { - return null; - } + return useComputed(() => { + if (!isInstrumentationActive()) { + return null; + } - if (totalUpdates <= 1) { - return null; - } + if (timelineState.value.totalUpdates <= 1) { + return null; + } - return ( - + {timelineState.value.isVisible ? ( + <> +
+ {sliderValues.value.leftValue} +
+ +
+ {sliderValues.value.rightValue} +
+ + ) : ( + 'View Re-renders History' + )} - { - isVisible - ? ( - <> -
- {sliderValues.leftValue} -
- -
- {sliderValues.rightValue} -
- - ) - : 'View Re-renders History' - } - - ); + ); + }); }); diff --git a/packages/scan/src/web/views/inspector/what-changed.tsx b/packages/scan/src/web/views/inspector/what-changed.tsx index 580fb921..bc28c426 100644 --- a/packages/scan/src/web/views/inspector/what-changed.tsx +++ b/packages/scan/src/web/views/inspector/what-changed.tsx @@ -19,7 +19,9 @@ import { Icon } from '~web/components/icon'; import { StickySection } from '~web/components/sticky-section'; import type { useMergedRefs } from '~web/hooks/use-merged-refs'; import { cn, throttle } from '~web/utils/helpers'; +import { useLazyRef } from '~web/utils/preact/use-lazy-ref'; import { DiffValueView } from './diff-value'; +import { createMap, createSet } from './factories'; import { type MinimalFiberInfo, timelineState } from './states'; import { Timeline } from './timeline'; import { @@ -379,13 +381,13 @@ interface SectionProps { const Section = memo(({ title, isExpanded }: SectionProps) => { const refFiberInfo = useRef(null); - const refLastUpdated = useRef(new Set()); - const refChangesValues = useRef(new Map()); + const refLastUpdated = useLazyRef(createSet); + const refChangesValues = useLazyRef(createMap); const refLatestChanges = useRef([]); const [changes, setChanges] = useState([]); - const [expandedFns, setExpandedFns] = useState(new Set()); - const [expandedEntries, setExpandedEntries] = useState(new Set()); + const [expandedFns, setExpandedFns] = useState(createSet); + const [expandedEntries, setExpandedEntries] = useState(createSet); useEffect(() => { const unsubscribe = timelineState.subscribe((state) => { diff --git a/packages/scan/src/web/widget/fps-meter.tsx b/packages/scan/src/web/widget/fps-meter.tsx index e94532e1..7b616c6f 100644 --- a/packages/scan/src/web/widget/fps-meter.tsx +++ b/packages/scan/src/web/widget/fps-meter.tsx @@ -1,26 +1,26 @@ -import { useEffect, useRef } from 'preact/hooks'; +import { useComputed, useSignal, useSignalEffect } from '@preact/signals'; import { getFPS } from '~core/instrumentation'; import { cn } from '~web/utils/helpers'; export const FpsMeter = () => { - const refFps = useRef(null); + const fps = useSignal(120); - useEffect(() => { + useSignalEffect(() => { const intervalId = setInterval(() => { - const fps = getFPS(); - let color = '#fff'; - if (fps) { - if (fps < 30) color = '#f87171'; - if (fps < 50) color = '#fbbf24'; - } - if (refFps.current) { - refFps.current.setAttribute('data-text', fps.toString()); - refFps.current.style.color = color; - } + fps.value = getFPS(); }, 100); return () => clearInterval(intervalId); - }, []); + }); + + const style = useComputed(() => { + let color = '#fff'; + if (fps.value < 30) color = '#f87171'; + if (fps.value < 50) color = '#fbbf24'; + return { + color, + }; + }); return ( { )} > - - FPS - + FPS ); };