import { type ReactNode, memo } from 'preact/compat'; import { type Dispatch, type StateUpdater, useEffect, useRef, useState, } from 'preact/hooks'; import { CopyToClipboard } from '~web/components/copy-to-clipboard'; import { Icon } from '~web/components/icon'; import { cn, throttle } from '~web/utils/helpers'; import { DiffValueView } from './diff-value'; import { timelineState } from './states'; import { AggregatedChanges, formatFunctionPreview, formatPath, getObjectDiff, safeGetValue, } from './utils'; import { calculateTotalChanges, useInspectedFiberChangeStore, } from './whats-changed/use-change-store'; import { getDisplayName, getType } from 'bippy'; import { Store } from '~core/index'; export type Setter = Dispatch>; export const WhatChanged = /* @__PURE__ */ memo(() => { const [isExpanded, setIsExpanded] = useState(true); const aggregatedChanges = useInspectedFiberChangeStore(); const [hasInitialized, setHasInitialized] = useState(false); const hasAnyChanges = calculateTotalChanges(aggregatedChanges) > 0; useEffect(() => { if (!hasInitialized && hasAnyChanges) { const timer = setTimeout(() => { setHasInitialized(true); requestAnimationFrame(() => { setIsExpanded(true); }); }, 0); return () => clearTimeout(timer); } }, [hasInitialized, hasAnyChanges]); const initializedContextChanges = new Map( Array.from(aggregatedChanges.contextChanges.entries()) .filter(([, value]) => value.kind === 'initialized') .map(([key, value]) => [ key, // oxlint-disable-next-line typescript/no-non-null-assertion value.kind === 'partially-initialized' ? null! : value.changes, ]), ); const fiber = Store.inspectState.value.kind === 'focused' ? Store.inspectState.value.fiber : null; if (!fiber) { // invariant return; } return ( <>
Why did{' '} {getDisplayName(fiber)}{' '} render? {!hasAnyChanges && (
No changes detected since selecting
The props, state, and context changes within your component will be reported here
)}
renderStateName( name, getDisplayName(getType(fiber)) ?? 'Unknown Component', ) } changes={aggregatedChanges.stateChanges} title="Changed State" isExpanded={isExpanded} />
); }); const renderStateName = (key: string, componentName: string) => { if (Number.isNaN(Number(key))) { return key; } const n = Number.parseInt(key); const getOrdinalSuffix = (num: number) => { const lastDigit = num % 10; const lastTwoDigits = num % 100; if (lastTwoDigits >= 11 && lastTwoDigits <= 13) { return 'th'; } switch (lastDigit) { case 1: return 'st'; case 2: return 'nd'; case 3: return 'rd'; default: return 'th'; } }; return ( {n} {getOrdinalSuffix(n)} hook{' '} called in {componentName} ); }; const WhatsChangedHeader = memo(() => { const refProps = useRef(null); const refState = useRef(null); const refContext = useRef(null); const refStats = useRef<{ isPropsChanged: boolean; isStateChanged: boolean; isContextChanged: boolean; }>({ isPropsChanged: false, isStateChanged: false, isContextChanged: false, }); useEffect(() => { const flash = throttle(() => { const flashElements = []; if (refProps.current?.dataset.flash === 'true') { flashElements.push(refProps.current); } if (refState.current?.dataset.flash === 'true') { flashElements.push(refState.current); } if (refContext.current?.dataset.flash === 'true') { flashElements.push(refContext.current); } for (const element of flashElements) { element.classList.remove('count-flash-white'); void element.offsetWidth; element.classList.add('count-flash-white'); } }, 400); const unsubscribe = timelineState.subscribe((state) => { if (!refProps.current || !refState.current || !refContext.current) { return; } const { currentIndex, updates } = state; const currentUpdate = updates[currentIndex]; if (!currentUpdate || currentIndex === 0) { return; } flash(); refStats.current = { isPropsChanged: (currentUpdate.props?.changes?.size ?? 0) > 0, isStateChanged: (currentUpdate.state?.changes?.size ?? 0) > 0, isContextChanged: (currentUpdate.context?.changes?.size ?? 0) > 0, }; if (refProps.current.dataset.flash !== 'true') { refProps.current.dataset.flash = refStats.current.isPropsChanged.toString(); } if (refState.current.dataset.flash !== 'true') { refState.current.dataset.flash = refStats.current.isStateChanged.toString(); } if (refContext.current.dataset.flash !== 'true') { refContext.current.dataset.flash = refStats.current.isContextChanged.toString(); } }); return unsubscribe; }, []); return ( ); }); interface SectionProps { title: string; isExpanded: boolean; // oxlint-disable-next-line typescript/no-explicit-any changes: Map; renderName?: (name: string) => ReactNode; } const identity = (x: T) => x; const Section = /* @__PURE__ */ memo( ({ title, changes, renderName = identity }: SectionProps) => { const [expandedFns, setExpandedFns] = useState(new Set()); const [expandedEntries, setExpandedEntries] = useState(new Set()); const entries = Array.from(changes.entries()); if (changes.size === 0) { return null; } return (
{title}
{entries.map(([entryKey, change]) => { const isEntryExpanded = expandedEntries.has(String(entryKey)); const { value: prevValue, error: prevError } = safeGetValue( change.previousValue, ); const { value: currValue, error: currError } = safeGetValue( change.currentValue, ); const diff = getObjectDiff(prevValue, currValue); return (
{prevError || currError ? ( ) : diff.changes.length > 0 ? ( ) : ( )}
); })}
); }, ); const AccessError = ({ prevError, currError, }: { prevError?: string; currError?: string; }) => { return ( <> {prevError && (
{prevError}
)} {currError && (
{currError}
)} ); }; const DiffChange = ({ diff, title, renderName, change, expandedFns, setExpandedFns, }: { diff: { changes: { path: string[]; prevValue: unknown; currentValue: unknown; }[]; }; title: string; renderName: (name: string) => ReactNode; change: { name: string }; expandedFns: Set; setExpandedFns: (updater: (prev: Set) => Set) => void; }) => { return diff.changes.map((diffChange, i) => { const { value: prevDiffValue, error: prevDiffError } = safeGetValue( diffChange.prevValue, ); const { value: currDiffValue, error: currDiffError } = safeGetValue( diffChange.currentValue, ); const isFunction = typeof prevDiffValue === 'function' || typeof currDiffValue === 'function'; let path: string | undefined; if (title === 'Props') { path = diffChange.path.length > 0 ? `${renderName(String(change.name))}.${formatPath(diffChange.path)}` : undefined; } if (title === 'State' && diffChange.path.length > 0) { path = `state.${formatPath(diffChange.path)}`; } if (!path) { path = formatPath(diffChange.path); } return (
{path &&
{path}
}
); }); }; const ReferenceOnlyChange = ({ prevValue, currValue, entryKey, expandedFns, setExpandedFns, }: { prevValue: unknown; currValue: unknown; entryKey: string | number; expandedFns: Set; setExpandedFns: (updater: (prev: Set) => Set) => void; }) => { return ( <>
- { const key = `${String(entryKey)}-prev`; setExpandedFns((prev) => { const next = new Set(prev); if (next.has(key)) { next.delete(key); } else { next.add(key); } return next; }); }} isNegative={true} />
+ { const key = `${String(entryKey)}-current`; setExpandedFns((prev) => { const next = new Set(prev); if (next.has(key)) { next.delete(key); } else { next.add(key); } return next; }); }} isNegative={false} />
{typeof currValue === 'object' && currValue !== null && (
Reference changed but objects are structurally the same
)} ); }; const CountBadge = ({ count, forceFlash, isFunction, showWarning, }: { count: number; forceFlash: boolean; isFunction: boolean; showWarning: boolean; }) => { const refIsFirstRender = useRef(true); const refBadge = useRef(null); const refPrevCount = useRef(count); useEffect(() => { const element = refBadge.current; if (!element || refPrevCount.current === count) { return; } element.classList.remove('count-flash'); void element.offsetWidth; element.classList.add('count-flash'); refPrevCount.current = count; }, [count]); useEffect(() => { if (refIsFirstRender.current) { refIsFirstRender.current = false; return; } if (forceFlash) { let timer = setTimeout(() => { refBadge.current?.classList.add('count-flash-white'); timer = setTimeout(() => { refBadge.current?.classList.remove('count-flash-white'); }, 300); }, 500); return () => { clearTimeout(timer); }; } }, [forceFlash]); return (
{showWarning && ( )} {isFunction && ( )} x{count}
); };