1+ import {
2+ untracked ,
3+ useComputed ,
4+ useSignal ,
5+ useSignalEffect ,
6+ } from '@preact/signals' ;
17import type { Fiber } from 'bippy' ;
2- import { useEffect , useMemo , useRef , useState } from 'preact/hooks' ;
8+ import { useRef } from 'preact/hooks' ;
39import { Store } from '~core/index' ;
410import { signalIsSettingsOpen } from '~web/state' ;
511import { cn , getExtendedDisplayName } from '~web/utils/helpers' ;
12+ import { constant } from '~web/utils/preact/constant' ;
613import { Icon } from '../icon' ;
714import { timelineState } from '../inspector/states' ;
815import { getOverrideMethods } from '../inspector/utils' ;
@@ -18,17 +25,16 @@ export const BtnReplay = () => {
1825 // },
1926 // });
2027
21- const [ canEdit , setCanEdit ] = useState ( false ) ;
22- const isSettingsOpen = signalIsSettingsOpen . value ;
28+ const canEdit = useSignal ( false ) ;
2329
24- useEffect ( ( ) => {
30+ useSignalEffect ( ( ) => {
2531 const { overrideProps } = getOverrideMethods ( ) ;
26- const canEdit = ! ! overrideProps ;
32+ const currentCanEdit = ! ! overrideProps ;
2733
2834 requestAnimationFrame ( ( ) => {
29- setCanEdit ( canEdit ) ;
35+ canEdit . value = currentCanEdit ;
3036 } ) ;
31- } , [ ] ) ;
37+ } ) ;
3238
3339 // const handleReplay = (e: MouseEvent) => {
3440 // e.stopPropagation();
@@ -63,20 +69,24 @@ export const BtnReplay = () => {
6369 // });
6470 // };
6571
66- if ( ! canEdit ) return null ;
67-
68- return (
69- < button
70- type = "button"
71- title = "Replay component"
72- // onClick={handleReplay}
73- className = { cn ( 'react-scan-replay-button' , {
74- 'opacity-0 pointer-events-none' : isSettingsOpen ,
75- } ) }
76- >
77- < Icon name = "icon-replay" />
78- </ button >
79- ) ;
72+ return useComputed ( ( ) => {
73+ if ( canEdit . value ) {
74+ return (
75+ < button
76+ type = "button"
77+ title = "Replay component"
78+ // onClick={handleReplay}
79+ // TODO(Alexis): can be granular
80+ className = { cn ( 'react-scan-replay-button' , {
81+ 'opacity-0 pointer-events-none' : signalIsSettingsOpen . value ,
82+ } ) }
83+ >
84+ < Icon name = "icon-replay" />
85+ </ button >
86+ ) ;
87+ }
88+ return null ;
89+ } ) ;
8090} ;
8191// const useSubscribeFocusedFiber = (onUpdate: () => void) => {
8292// // biome-ignore lint/correctness/useExhaustiveDependencies: no deps
@@ -100,121 +110,114 @@ export const BtnReplay = () => {
100110const HeaderInspect = ( ) => {
101111 const refReRenders = useRef < HTMLSpanElement > ( null ) ;
102112 const refTiming = useRef < HTMLSpanElement > ( null ) ;
103- const isSettingsOpen = signalIsSettingsOpen . value ;
104- const [ currentFiber , setCurrentFiber ] = useState < Fiber | null > ( null ) ;
113+ const currentFiber = useSignal < Fiber | null > ( null ) ;
105114
106- useEffect ( ( ) => {
107- const unSubState = Store . inspectState . subscribe ( ( state ) => {
108- if ( state . kind !== 'focused' ) return ;
115+ // TODO(Alexis): can be computed?
116+ useSignalEffect ( ( ) => {
117+ const state = Store . inspectState . value ;
109118
110- const fiber = state . fiber ;
111- if ( ! fiber ) return ;
119+ if ( state . kind !== 'focused' ) {
120+ return ;
121+ }
112122
113- setCurrentFiber ( fiber ) ;
114- } ) ;
123+ currentFiber . value = state . fiber ;
124+ } ) ;
115125
116- return unSubState ;
117- } , [ ] ) ;
118-
119- useEffect ( ( ) => {
120- const unSubTimeline = timelineState . subscribe ( ( state ) => {
121- if ( Store . inspectState . value . kind !== 'focused' ) return ;
122- if ( ! refReRenders . current || ! refTiming . current ) return ;
123-
124- const { totalUpdates, currentIndex, updates, isVisible, windowOffset } =
125- state ;
126-
127- const reRenders = Math . max ( 0 , totalUpdates - 1 ) ;
128- const headerText = isVisible
129- ? `#${ windowOffset + currentIndex } Re-render`
130- : `${ reRenders } Re-renders` ;
131-
132- let formattedTime : string | undefined ;
133- if ( reRenders > 0 && currentIndex >= 0 && currentIndex < updates . length ) {
134- const time = updates [ currentIndex ] ?. fiberInfo ?. selfTime ;
135- formattedTime =
136- time > 0
137- ? time < 0.1 - Number . EPSILON
138- ? '< 0.1ms'
139- : `${ Number ( time . toFixed ( 1 ) ) } ms`
140- : undefined ;
141- }
142-
143- refReRenders . current . dataset . text = `${ headerText } ${ reRenders > 0 && formattedTime ? ' •' : '' } ` ;
144- if ( formattedTime ) {
145- refTiming . current . dataset . text = formattedTime ;
146- }
147- } ) ;
126+ useSignalEffect ( ( ) => {
127+ const state = timelineState . value ;
148128
149- return unSubTimeline ;
150- } , [ ] ) ;
129+ if ( untracked ( ( ) => Store . inspectState . value . kind !== 'focused' ) ) {
130+ return ;
131+ }
132+ if ( ! refReRenders . current || ! refTiming . current ) return ;
133+
134+ const { totalUpdates, currentIndex, updates, isVisible, windowOffset } =
135+ state ;
136+
137+ const reRenders = Math . max ( 0 , totalUpdates - 1 ) ;
138+ const headerText = isVisible
139+ ? `#${ windowOffset + currentIndex } Re-render`
140+ : `${ reRenders } Re-renders` ;
141+
142+ let formattedTime : string | undefined ;
143+ if ( reRenders > 0 && currentIndex >= 0 && currentIndex < updates . length ) {
144+ const time = updates [ currentIndex ] ?. fiberInfo ?. selfTime ;
145+ formattedTime =
146+ time > 0
147+ ? time < 0.1 - Number . EPSILON
148+ ? '< 0.1ms'
149+ : `${ Number ( time . toFixed ( 1 ) ) } ms`
150+ : undefined ;
151+ }
151152
152- const componentName = useMemo ( ( ) => {
153- if ( ! currentFiber ) return null ;
154- const { name, wrappers, wrapperTypes } = getExtendedDisplayName ( currentFiber ) ;
153+ // TODO(Alexis): use props instead?
154+ refReRenders . current . dataset . text = `${ headerText } ${ reRenders > 0 && formattedTime ? ' •' : '' } ` ;
155+ if ( formattedTime ) {
156+ refTiming . current . dataset . text = formattedTime ;
157+ }
158+ } ) ;
159+
160+ const componentName = useComputed ( ( ) => {
161+ if ( ! currentFiber . value ) {
162+ return null ;
163+ }
164+ const { name, wrappers, wrapperTypes } = getExtendedDisplayName (
165+ currentFiber . value ,
166+ ) ;
155167
156168 const title = wrappers . length
157169 ? `${ wrappers . join ( '(' ) } (${ name } )${ ')' . repeat ( wrappers . length ) } `
158- : name ?? '' ;
170+ : ( name ?? '' ) ;
159171
160172 const firstWrapperType = wrapperTypes [ 0 ] ;
173+
174+ // TODO(Alexis): can be granular
161175 return (
162- < span
163- title = { title }
164- className = "flex items-center gap-x-1"
165- >
176+ < span title = { title } className = "flex items-center gap-x-1" >
166177 { name ?? 'Unknown' }
167178 < span
168179 title = { firstWrapperType ?. title }
169180 className = "flex items-center gap-x-1 text-[10px] text-purple-400"
170181 >
171- {
172- ! ! firstWrapperType && (
173- < >
174- < span
175- key = { firstWrapperType . type }
176- className = { cn (
177- 'rounded py-[1px] px-1' ,
178- 'truncate' ,
179- {
180- 'bg-purple-800 text-neutral-400' : firstWrapperType . compiler ,
181- 'bg-neutral-700 text-neutral-300' : ! firstWrapperType . compiler ,
182- 'bg-[#5f3f9a] text-white' : firstWrapperType . type === 'memo' ,
183- }
184- ) }
185- >
186- { firstWrapperType . type }
187- </ span >
188- { firstWrapperType . compiler && (
189- < span className = "text-yellow-300" > ✨</ span >
190- ) }
191- </ >
192- )
193- }
182+ { ! ! firstWrapperType && (
183+ < >
184+ < span
185+ key = { firstWrapperType . type }
186+ className = { cn ( 'rounded py-[1px] px-1' , 'truncate' , {
187+ 'bg-purple-800 text-neutral-400' : firstWrapperType . compiler ,
188+ 'bg-neutral-700 text-neutral-300' : ! firstWrapperType . compiler ,
189+ 'bg-[#5f3f9a] text-white' : firstWrapperType . type === 'memo' ,
190+ } ) }
191+ >
192+ { firstWrapperType . type }
193+ </ span >
194+ { firstWrapperType . compiler && (
195+ < span className = "text-yellow-300" > ✨</ span >
196+ ) }
197+ </ >
198+ ) }
194199 </ span >
195- {
196- wrapperTypes . length > 1 && (
197- < span className = "text-[10px] text-neutral-400" >
198- ×{ wrapperTypes . length - 1 }
199- </ span >
200- )
201- }
202- < samp className = "text-neutral-500" >
203- { ' • ' }
204- </ samp >
200+ { wrapperTypes . length > 1 && (
201+ < span className = "text-[10px] text-neutral-400" >
202+ ×{ wrapperTypes . length - 1 }
203+ </ span >
204+ ) }
205+ < samp className = "text-neutral-500" > { ' • ' } </ samp >
205206 </ span >
206207 ) ;
207- } , [ currentFiber ] ) ;
208+ } ) ;
208209
209210 return (
210211 < div
211- className = { cn (
212- 'absolute inset-0 flex items-center gap-x-2' ,
213- 'translate-y-0' ,
214- 'transition-transform duration-300' ,
215- {
216- '-translate-y-[200%]' : isSettingsOpen ,
217- } ,
212+ className = { useComputed ( ( ) =>
213+ cn (
214+ 'absolute inset-0 flex items-center gap-x-2' ,
215+ 'translate-y-0' ,
216+ 'transition-transform duration-300' ,
217+ {
218+ '-translate-y-[200%]' : signalIsSettingsOpen . value ,
219+ } ,
220+ ) ,
218221 ) }
219222 >
220223 { componentName }
@@ -230,25 +233,22 @@ const HeaderInspect = () => {
230233 ) ;
231234} ;
232235
233- const HeaderSettings = ( ) => {
234- const isSettingsOpen = signalIsSettingsOpen . value ;
235- return (
236- < span
237- data-text = "Settings"
238- className = { cn (
239- 'absolute inset-0 flex items-center' ,
240- 'with-data-text' ,
241- '-translate-y-[200%]' ,
242- 'transition-transform duration-300' ,
243- {
244- 'translate-y-0' : isSettingsOpen ,
245- } ,
246- ) }
247- />
236+ const HeaderSettings = constant ( ( ) => {
237+ const className = useComputed ( ( ) =>
238+ cn (
239+ 'absolute inset-0 flex items-center' ,
240+ 'with-data-text' ,
241+ '-translate-y-[200%]' ,
242+ 'transition-transform duration-300' ,
243+ {
244+ 'translate-y-0' : signalIsSettingsOpen . value ,
245+ } ,
246+ ) ,
248247 ) ;
249- } ;
248+ return < span data-text = "Settings" className = { className } /> ;
249+ } ) ;
250250
251- export const Header = ( ) => {
251+ export const Header = constant ( ( ) => {
252252 const handleClose = ( ) => {
253253 if ( signalIsSettingsOpen . value ) {
254254 signalIsSettingsOpen . value = false ;
@@ -279,4 +279,4 @@ export const Header = () => {
279279 </ button >
280280 </ div >
281281 ) ;
282- } ;
282+ } ) ;
0 commit comments