1- import { useRef , useEffect , useCallback , useState } from "preact/hooks" ;
1+ import { useRef , useEffect , useCallback } from "preact/hooks" ;
22import { getDisplayName } from 'bippy' ;
3- import { cn } from "@web-utils/helpers" ;
43import { Store } from "../../../.." ;
54import { getCompositeComponentFromElement , getOverrideMethods } from "../../inspect-element/utils" ;
65import { replayComponent } from "../../inspect-element/view-state" ;
76import { Icon } from "../icon" ;
7+ import { debounce } from "../../utils/helpers" ;
88
9- export const Header = ( ) => {
10- const inspectState = Store . inspectState . value ;
11- const refComponentName = useRef < HTMLSpanElement > ( null ) ;
12- const refMetrics = useRef < HTMLSpanElement > ( null ) ;
13- const [ isReplaying , setIsReplaying ] = useState ( false ) ;
14-
15- useEffect ( ( ) => {
16- const updateMetrics = ( ) => {
17- if ( ! refComponentName . current || ! refMetrics . current ) return ;
18- if ( inspectState . kind !== 'focused' ) return ;
19-
20- if ( ! document . contains ( inspectState . focusedDomElement ) ) {
21- if ( Store . inspectState . value . propContainer ) {
22- Store . inspectState . value = {
23- kind : 'inspect-off' ,
24- propContainer : Store . inspectState . value . propContainer ,
25- } ;
26- }
27- return ;
28- }
9+ const BtnReplay = ( ) => {
10+ const refBtnReplay = useRef < HTMLButtonElement > ( null ) ;
11+ const refIsReplaying = useRef ( false ) ;
12+ const timeoutRef = useRef < number > ( ) ;
2913
14+ const { overrideProps, overrideHookState } = getOverrideMethods ( ) ;
15+ const canEdit = ! overrideProps ;
3016
31- const { parentCompositeFiber } = getCompositeComponentFromElement ( inspectState . focusedDomElement ) ;
17+ const handleReplay = useCallback ( ( e : MouseEvent ) => {
18+ e . stopPropagation ( ) ;
3219
33- if ( ! parentCompositeFiber ) return ;
20+ const inspectState = Store . inspectState . value ;
21+ if ( refIsReplaying . current || inspectState . kind !== 'focused' ) return ;
3422
35- const reportDataFiber =
36- Store . reportData . get ( parentCompositeFiber ) ??
37- ( parentCompositeFiber . alternate
38- ? Store . reportData . get ( parentCompositeFiber . alternate )
39- : null ) ;
23+ const { parentCompositeFiber } = getCompositeComponentFromElement ( inspectState . focusedDomElement ) ;
24+ if ( ! parentCompositeFiber || ! overrideProps || ! overrideHookState ) return ;
4025
41- const componentName = getDisplayName ( parentCompositeFiber . type ) ?? 'Unknown' ;
26+ refIsReplaying . current = true ;
27+ refBtnReplay . current ?. classList . add ( 'disabled' ) ;
4228
43- const renderCount = reportDataFiber ?. count ?? 0 ;
44- const renderTime = reportDataFiber ?. time ?? 0 ;
29+ void replayComponent ( parentCompositeFiber ) . finally ( ( ) => {
30+ if ( timeoutRef . current ) {
31+ clearTimeout ( timeoutRef . current ) ;
32+ }
4533
46- refComponentName . current . textContent = componentName ;
47- refMetrics . current . textContent = renderCount > 0
48- ? `${ renderCount } renders${ renderTime > 0 ? ` • ${ renderTime . toFixed ( 2 ) } ms` : '' } `
49- : '' ;
50- } ;
34+ const cleanup = ( ) => {
35+ refIsReplaying . current = false ;
36+ refBtnReplay . current ?. classList . remove ( 'disabled' ) ;
37+ } ;
5138
52- const unsubscribe = Store . lastReportTime . subscribe ( updateMetrics ) ;
39+ if ( document . hidden ) {
40+ cleanup ( ) ;
41+ } else {
42+ timeoutRef . current = window . setTimeout ( cleanup , 300 ) ;
43+ }
44+ } ) ;
45+ } , [ ] ) ;
5346
47+ useEffect ( ( ) => {
5448 return ( ) => {
55- unsubscribe ( ) ;
49+ if ( timeoutRef . current ) {
50+ clearTimeout ( timeoutRef . current ) ;
51+ }
5652 } ;
57- } , [ inspectState ] ) ;
53+ } , [ ] ) ;
54+
55+ if ( ! canEdit ) return null ;
56+
57+ return (
58+ < button
59+ ref = { refBtnReplay }
60+ title = "Replay component"
61+ className = "react-scan-replay-button"
62+ onClick = { handleReplay }
63+ >
64+ < Icon name = "icon-replay" />
65+ </ button >
66+ ) ;
67+ } ;
68+
69+ export const Header = ( ) => {
70+ const refComponentName = useRef < HTMLSpanElement > ( null ) ;
71+ const refMetrics = useRef < HTMLSpanElement > ( null ) ;
5872
5973 const handleClose = useCallback ( ( ) => {
6074 if ( Store . inspectState . value . propContainer ) {
@@ -65,64 +79,79 @@ export const Header = () => {
6579 }
6680 } , [ ] ) ;
6781
68- const handleReplay = useCallback ( ( e : MouseEvent ) => {
69- void ( async ( ) => {
70- e . stopPropagation ( ) ;
71- if ( isReplaying || inspectState . kind !== 'focused' ) return ;
82+ const updateHeaderContent = useCallback ( ( ) => {
83+ const inspectState = Store . inspectState . value ;
84+ if ( inspectState . kind !== 'focused' ) return ;
85+
86+ const focusedDomElement = inspectState . focusedDomElement ;
87+ if ( ! refComponentName . current || ! refMetrics . current || ! focusedDomElement ) return ;
88+
89+ const { parentCompositeFiber } = getCompositeComponentFromElement ( focusedDomElement ) ;
90+ if ( ! parentCompositeFiber ) return ;
7291
73- const { parentCompositeFiber } = getCompositeComponentFromElement ( inspectState . focusedDomElement ) ;
74- if ( ! parentCompositeFiber ) return ;
92+ const currentComponentName = refComponentName . current . dataset . text ;
93+ const currentMetrics = refMetrics . current . dataset . text ;
7594
76- const { overrideProps, overrideHookState } = getOverrideMethods ( ) ;
77- if ( ! overrideProps || ! overrideHookState ) return ;
95+ const fiber = parentCompositeFiber . alternate ?? parentCompositeFiber ;
96+ const reportData = Store . reportData . get ( fiber ) ;
97+ const componentName = getDisplayName ( parentCompositeFiber . type ) ?? 'Unknown' ;
7898
79- setIsReplaying ( true ) ;
99+ if ( componentName === currentComponentName && reportData ?. count === 0 ) {
100+ return ;
101+ }
80102
81- try {
82- await replayComponent ( parentCompositeFiber ) ;
83- } finally {
84- setTimeout ( ( ) => {
85- setIsReplaying ( false ) ;
86- } , 300 ) ;
103+ const renderCount = reportData ?. count ?? 0 ;
104+ const renderTime = reportData ?. time ?? 0 ;
105+ const newMetrics = renderCount > 0
106+ ? `${ renderCount } renders${ renderTime > 0 ? ` • ${ renderTime . toFixed ( 2 ) } ms` : '' } `
107+ : '' ;
108+
109+ if ( componentName !== currentComponentName || newMetrics !== currentMetrics ) {
110+ requestAnimationFrame ( ( ) => {
111+ if ( ! refComponentName . current || ! refMetrics . current ) return ;
112+ refComponentName . current . dataset . text = componentName ;
113+ refMetrics . current . dataset . text = newMetrics ;
114+ } ) ;
115+ }
116+ } , [ ] ) ;
117+
118+ useEffect ( ( ) => {
119+ const debouncedUpdate = debounce ( updateHeaderContent , 16 , { leading : true } ) ;
120+
121+ const unsubscribeLastReportTime = Store . lastReportTime . subscribe ( debouncedUpdate ) ;
122+ const unsubscribeStoreInspectState = Store . inspectState . subscribe ( state => {
123+ if ( state . kind === 'focused' ) {
124+ debouncedUpdate ( ) ;
87125 }
88- } ) ( ) ;
89- } , [ inspectState , isReplaying ] ) ;
126+ } ) ;
90127
91- const { overrideProps } = getOverrideMethods ( ) ;
92- const canEdit = ! ! overrideProps ;
128+ return ( ) => {
129+ unsubscribeLastReportTime ( ) ;
130+ unsubscribeStoreInspectState ( ) ;
131+ debouncedUpdate . cancel ?.( ) ;
132+ } ;
133+ } , [ updateHeaderContent ] ) ;
93134
94135 return (
95- < div
96- className = { cn (
97- "react-scan-header" ,
98- "flex" ,
99- "min-h-9" ,
100- 'whitespace-nowrap' ,
101- "overflow-hidden" ,
102- ) }
103- >
104- < div className = "react-scan-header-left overflow-hidden" >
105- < span ref = { refComponentName } className = "react-scan-component-name" />
106- < span ref = { refMetrics } className = "react-scan-metrics" />
107- </ div >
108- < div class = "react-scan-header-right" >
109- { canEdit && (
110- < button
111- title = "Replay component"
112- class = { `react-scan-replay-button${ isReplaying ? ' disabled' : '' } ` }
113- onClick = { handleReplay }
114- >
115- < Icon name = "icon-replay" />
116- </ button >
117- ) }
118- < button
119- title = "Close"
120- class = "react-scan-close-button"
121- onClick = { handleClose }
122- >
123- < Icon name = "icon-close" />
124- </ button >
125- </ div >
136+ < div className = "react-scan-header" >
137+ < span
138+ ref = { refComponentName }
139+ className = "with-data-text"
140+ />
141+ < span
142+ ref = { refMetrics }
143+ className = "with-data-text mr-auto !overflow-visible text-xs text-[#888]"
144+ />
145+
146+ < BtnReplay />
147+
148+ < button
149+ title = "Close"
150+ class = "react-scan-close-button"
151+ onClick = { handleClose }
152+ >
153+ < Icon name = "icon-close" />
154+ </ button >
126155 </ div >
127156 ) ;
128157} ;
0 commit comments