@@ -4,59 +4,60 @@ import { Store } from "../../../..";
44import { getCompositeComponentFromElement , getOverrideMethods } from "../../inspect-element/utils" ;
55import { replayComponent } from "../../inspect-element/view-state" ;
66import { Icon } from "../icon" ;
7- import { debounce } from "../../utils/helpers" ;
7+ import type { States } from "../../inspect-element/inspect-state-machine" ;
8+
9+ const THROTTLE_MS = 32 ;
10+ const REPLAY_DELAY_MS = 300 ;
811
912const BtnReplay = ( ) => {
10- const refBtnReplay = useRef < HTMLButtonElement > ( null ) ;
11- const refIsReplaying = useRef ( false ) ;
12- const timeoutRef = useRef < number > ( ) ;
13+ const replayState = useRef ( {
14+ isReplaying : false ,
15+ timeoutId : undefined as TTimer ,
16+ toggleDisabled ( disabled : boolean , button : HTMLElement ) {
17+ button . classList [ disabled ? 'add' : 'remove' ] ( 'disabled' ) ;
18+ }
19+ } ) ;
1320
1421 const { overrideProps, overrideHookState } = getOverrideMethods ( ) ;
15- const canEdit = ! overrideProps ;
22+ const canEdit = ! ! overrideProps ;
1623
17- const handleReplay = useCallback ( ( e : MouseEvent ) => {
24+ const handleReplay = ( e : MouseEvent ) => {
1825 e . stopPropagation ( ) ;
26+ const state = replayState . current ;
27+ const button = e . currentTarget as HTMLElement ;
1928
2029 const inspectState = Store . inspectState . value ;
21- if ( refIsReplaying . current || inspectState . kind !== 'focused' ) return ;
30+ if ( state . isReplaying || inspectState . kind !== 'focused' ) return ;
2231
2332 const { parentCompositeFiber } = getCompositeComponentFromElement ( inspectState . focusedDomElement ) ;
2433 if ( ! parentCompositeFiber || ! overrideProps || ! overrideHookState ) return ;
2534
26- refIsReplaying . current = true ;
27- refBtnReplay . current ?. classList . add ( 'disabled' ) ;
28-
29- void replayComponent ( parentCompositeFiber ) . finally ( ( ) => {
30- if ( timeoutRef . current ) {
31- clearTimeout ( timeoutRef . current ) ;
32- }
33-
34- const cleanup = ( ) => {
35- refIsReplaying . current = false ;
36- refBtnReplay . current ?. classList . remove ( 'disabled' ) ;
37- } ;
38-
39- if ( document . hidden ) {
40- cleanup ( ) ;
41- } else {
42- timeoutRef . current = window . setTimeout ( cleanup , 300 ) ;
43- }
44- } ) ;
45- } , [ ] ) ;
46-
47- useEffect ( ( ) => {
48- return ( ) => {
49- if ( timeoutRef . current ) {
50- clearTimeout ( timeoutRef . current ) ;
51- }
52- } ;
53- } , [ ] ) ;
35+ state . isReplaying = true ;
36+ state . toggleDisabled ( true , button ) ;
37+
38+ void replayComponent ( parentCompositeFiber )
39+ . catch ( ( ) => void 0 )
40+ . finally ( ( ) => {
41+ if ( state . timeoutId ) {
42+ clearTimeout ( state . timeoutId ) ;
43+ state . timeoutId = undefined ;
44+ }
45+ if ( document . hidden ) {
46+ state . isReplaying = false ;
47+ state . toggleDisabled ( false , button ) ;
48+ } else {
49+ state . timeoutId = setTimeout ( ( ) => {
50+ state . isReplaying = false ;
51+ state . toggleDisabled ( false , button ) ;
52+ } , REPLAY_DELAY_MS ) ;
53+ }
54+ } ) ;
55+ } ;
5456
5557 if ( ! canEdit ) return null ;
5658
5759 return (
5860 < button
59- ref = { refBtnReplay }
6061 title = "Replay component"
6162 className = "react-scan-replay-button"
6263 onClick = { handleReplay }
@@ -67,85 +68,157 @@ const BtnReplay = () => {
6768} ;
6869
6970export const Header = ( ) => {
70- const refComponentName = useRef < HTMLSpanElement > ( null ) ;
71- const refMetrics = useRef < HTMLSpanElement > ( null ) ;
72-
73- const handleClose = useCallback ( ( ) => {
71+ const headerState = useRef ( {
72+ refs : {
73+ componentName : null as HTMLSpanElement | null ,
74+ metrics : null as HTMLSpanElement | null ,
75+ } ,
76+ timers : {
77+ update : undefined as TTimer ,
78+ raf : 0 as number
79+ } ,
80+ values : {
81+ componentName : '' ,
82+ metrics : '' ,
83+ lastUpdate : 0 ,
84+ pendingUpdate : false ,
85+ fiber : null as any
86+ } ,
87+ mounted : true
88+ } ) ;
89+
90+ const handleClose = ( ) => {
7491 if ( Store . inspectState . value . propContainer ) {
7592 Store . inspectState . value = {
7693 kind : 'inspect-off' ,
7794 propContainer : Store . inspectState . value . propContainer ,
7895 } ;
7996 }
80- } , [ ] ) ;
97+ } ;
98+
99+ const formatMetrics = ( count : number , time ?: number ) =>
100+ `${ count } renders${ time ? ` • ${ time . toFixed ( 2 ) } ms` : '' } ` ;
81101
82102 const updateHeaderContent = useCallback ( ( ) => {
103+ const state = headerState . current ;
104+ if ( ! state . mounted ) return ;
105+
83106 const inspectState = Store . inspectState . value ;
84107 if ( inspectState . kind !== 'focused' ) return ;
85108
86109 const focusedDomElement = inspectState . focusedDomElement ;
87- if ( ! refComponentName . current || ! refMetrics . current || ! focusedDomElement ) return ;
110+ if ( ! focusedDomElement || ! state . refs . componentName || ! state . refs . metrics ) return ;
88111
89112 const { parentCompositeFiber } = getCompositeComponentFromElement ( focusedDomElement ) ;
90113 if ( ! parentCompositeFiber ) return ;
91114
92- const currentComponentName = refComponentName . current . dataset . text ;
93- const currentMetrics = refMetrics . current . dataset . text ;
94-
95115 const fiber = parentCompositeFiber . alternate ?? parentCompositeFiber ;
96- const reportData = Store . reportData . get ( fiber ) ;
97- const componentName = getDisplayName ( parentCompositeFiber . type ) ?? 'Unknown' ;
98116
99- if ( componentName === currentComponentName && reportData ?. count === 0 ) {
100- return ;
117+ if ( fiber !== state . values . fiber ) {
118+ state . values . fiber = fiber ;
119+ state . values . componentName = getDisplayName ( parentCompositeFiber . type ) ?? 'Unknown' ;
101120 }
102121
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 ;
122+ const reportData = Store . reportData . get ( fiber ) ;
123+
124+ if ( ! reportData ?. count ) return ;
125+ const newMetrics = formatMetrics ( reportData . count , reportData . time ) ;
126+ if ( newMetrics === state . values . metrics && ! state . values . pendingUpdate ) return ;
127+
128+ if ( ! state . values . pendingUpdate ) {
129+ state . values . pendingUpdate = true ;
130+ cancelAnimationFrame ( state . timers . raf ) ;
131+ state . timers . raf = requestAnimationFrame ( ( ) => {
132+ if ( state . refs . componentName && state . refs . metrics ) {
133+ state . refs . componentName . dataset . text = state . values . componentName ;
134+ state . refs . metrics . dataset . text = newMetrics ;
135+ state . values . metrics = newMetrics ;
136+ state . values . lastUpdate = Date . now ( ) ;
137+ state . values . pendingUpdate = false ;
138+ state . timers . raf = 0 ;
139+ }
114140 } ) ;
115141 }
116142 } , [ ] ) ;
117143
144+ const scheduleUpdate = useCallback ( ( ) => {
145+ const state = headerState . current ;
146+ const now = Date . now ( ) ;
147+ const timeSinceLastUpdate = now - state . values . lastUpdate ;
148+
149+ if ( timeSinceLastUpdate < THROTTLE_MS ) return ;
150+
151+ if ( state . timers . update ) {
152+ clearTimeout ( state . timers . update ) ;
153+ state . timers . update = undefined ;
154+ }
155+
156+ state . timers . update = setTimeout ( updateHeaderContent , THROTTLE_MS ) ;
157+ } , [ updateHeaderContent ] ) ;
158+
159+ const handleInspectStateChange = useCallback ( ( newState : States ) => {
160+ const state = headerState . current ;
161+ if ( ! state . mounted ) return ;
162+
163+ if ( state . timers . update ) {
164+ clearTimeout ( state . timers . update ) ;
165+ state . timers . update = undefined ;
166+ }
167+ if ( state . timers . raf ) {
168+ cancelAnimationFrame ( state . timers . raf ) ;
169+ state . timers . raf = 0 ;
170+ }
171+ state . values . pendingUpdate = false ;
172+
173+ if ( newState . kind === 'focused' ) {
174+ updateHeaderContent ( ) ;
175+ }
176+ } , [ updateHeaderContent ] ) ;
177+
118178 useEffect ( ( ) => {
119- const unsubscribeLastReportTime = Store . lastReportTime . subscribe ( updateHeaderContent ) ;
120- const unsubscribeStoreInspectState = Store . inspectState . subscribe ( state => {
121- if ( state . kind === 'focused' ) {
122- updateHeaderContent ( ) ;
123- }
124- } ) ;
179+ const state = headerState . current ;
180+
181+ Store . lastReportTime . subscribe ( scheduleUpdate ) ;
182+ Store . inspectState . subscribe ( handleInspectStateChange ) ;
125183
126184 return ( ) => {
127- unsubscribeLastReportTime ( ) ;
128- unsubscribeStoreInspectState ( ) ;
185+ state . mounted = false ;
186+ if ( state . timers . update ) {
187+ clearTimeout ( state . timers . update ) ;
188+ state . timers . update = undefined ;
189+ }
190+ if ( state . timers . raf ) {
191+ cancelAnimationFrame ( state . timers . raf ) ;
192+ state . timers . raf = 0 ;
193+ }
194+ state . values . pendingUpdate = false ;
129195 } ;
130- } , [ updateHeaderContent ] ) ;
196+ } , [ scheduleUpdate , handleInspectStateChange ] ) ;
197+
198+ const setComponentNameRef = useCallback ( ( node : HTMLSpanElement | null ) => {
199+ headerState . current . refs . componentName = node ;
200+ } , [ ] ) ;
201+
202+ const setMetricsRef = useCallback ( ( node : HTMLSpanElement | null ) => {
203+ headerState . current . refs . metrics = node ;
204+ } , [ ] ) ;
131205
132206 return (
133207 < div className = "react-scan-header" >
134208 < span
135- ref = { refComponentName }
209+ ref = { setComponentNameRef }
136210 className = "with-data-text"
137211 />
138212 < span
139- ref = { refMetrics }
213+ ref = { setMetricsRef }
140214 className = "with-data-text mr-auto !overflow-visible text-xs text-[#888]"
141215 />
142216
143- { /* fixme: render replay button causes large amounts of cpu usage when idle */ }
144- { /* <BtnReplay /> */ }
217+ < BtnReplay />
145218
146219 < button
147220 title = "Close"
148- class = "react-scan-close-button ml-auto "
221+ class = "react-scan-close-button"
149222 onClick = { handleClose }
150223 >
151224 < Icon name = "icon-close" />
0 commit comments