@@ -232,6 +232,10 @@ const CodeBlock = memo(
232232 const copyButtonWrapperRef = useRef < HTMLDivElement > ( null )
233233 const { showCopyFeedback, copyWithFeedback } = useCopyToClipboard ( )
234234 const { t } = useAppTranslation ( )
235+ const isMountedRef = useRef ( true )
236+ const buttonPositionTimeoutRef = useRef < NodeJS . Timeout | null > ( null )
237+ const collapseTimeout1Ref = useRef < NodeJS . Timeout | null > ( null )
238+ const collapseTimeout2Ref = useRef < NodeJS . Timeout | null > ( null )
235239
236240 // Update current language when prop changes, but only if user hasn't
237241 // made a selection.
@@ -243,17 +247,40 @@ const CodeBlock = memo(
243247 }
244248 } , [ language , currentLanguage ] )
245249
250+ // Manage mounted state
251+ useEffect ( ( ) => {
252+ isMountedRef . current = true
253+ return ( ) => {
254+ isMountedRef . current = false
255+ }
256+ } , [ ] )
257+
258+ // Cleanup for collapse/expand timeouts
259+ useEffect ( ( ) => {
260+ return ( ) => {
261+ if ( collapseTimeout1Ref . current ) {
262+ clearTimeout ( collapseTimeout1Ref . current )
263+ }
264+ if ( collapseTimeout2Ref . current ) {
265+ clearTimeout ( collapseTimeout2Ref . current )
266+ }
267+ }
268+ } , [ ] )
269+
246270 // Syntax highlighting with cached Shiki instance.
247271 useEffect ( ( ) => {
248272 const fallback = `<pre style="padding: 0; margin: 0;"><code class="hljs language-${ currentLanguage || "txt" } ">${ source || "" } </code></pre>`
249273
250274 const highlight = async ( ) => {
251275 // Show plain text if language needs to be loaded.
252276 if ( currentLanguage && ! isLanguageLoaded ( currentLanguage ) ) {
253- setHighlightedCode ( fallback )
277+ if ( isMountedRef . current ) {
278+ setHighlightedCode ( fallback )
279+ }
254280 }
255281
256282 const highlighter = await getHighlighter ( currentLanguage )
283+ if ( ! isMountedRef . current ) return
257284
258285 const html = await highlighter . codeToHtml ( source || "" , {
259286 lang : currentLanguage || "txt" ,
@@ -277,13 +304,18 @@ const CodeBlock = memo(
277304 } ,
278305 ] as ShikiTransformer [ ] ,
279306 } )
307+ if ( ! isMountedRef . current ) return
280308
281- setHighlightedCode ( html )
309+ if ( isMountedRef . current ) {
310+ setHighlightedCode ( html )
311+ }
282312 }
283313
284314 highlight ( ) . catch ( ( e ) => {
285315 console . error ( "[CodeBlock] Syntax highlighting error:" , e , "\nStack trace:" , e . stack )
286- setHighlightedCode ( fallback )
316+ if ( isMountedRef . current ) {
317+ setHighlightedCode ( fallback )
318+ }
287319 } )
288320 } , [ source , currentLanguage , collapsedHeight ] )
289321
@@ -455,8 +487,15 @@ const CodeBlock = memo(
455487 // Update button position and scroll when highlightedCode changes
456488 useEffect ( ( ) => {
457489 if ( highlightedCode ) {
490+ // Clear any existing timeout before setting a new one
491+ if ( buttonPositionTimeoutRef . current ) {
492+ clearTimeout ( buttonPositionTimeoutRef . current )
493+ }
458494 // Update button position
459- setTimeout ( updateCodeBlockButtonPosition , 0 )
495+ buttonPositionTimeoutRef . current = setTimeout ( ( ) => {
496+ updateCodeBlockButtonPosition ( )
497+ buttonPositionTimeoutRef . current = null // Optional: Clear ref after execution
498+ } , 0 )
460499
461500 // Scroll to bottom if needed (immediately after Shiki updates)
462501 if ( shouldScrollAfterHighlightRef . current ) {
@@ -479,6 +518,12 @@ const CodeBlock = memo(
479518 shouldScrollAfterHighlightRef . current = false
480519 }
481520 }
521+ // Cleanup function for this effect
522+ return ( ) => {
523+ if ( buttonPositionTimeoutRef . current ) {
524+ clearTimeout ( buttonPositionTimeoutRef . current )
525+ }
526+ }
482527 } , [ highlightedCode , updateCodeBlockButtonPosition ] )
483528
484529 // Advanced inertial scroll chaining
@@ -682,23 +727,30 @@ const CodeBlock = memo(
682727 { showCollapseButton && (
683728 < CodeBlockButton
684729 onClick = { ( ) => {
685- // Get the current code block element and scrollable container
686- const codeBlock = codeBlockRef . current
687- const scrollContainer = document . querySelector ( '[data-virtuoso-scroller="true"]' )
688- if ( ! codeBlock || ! scrollContainer ) return
689-
730+ // Get the current code block element
731+ const codeBlock = codeBlockRef . current // Capture ref early
690732 // Toggle window shade state
691733 setWindowShade ( ! windowShade )
692734
735+ // Clear any previous timeouts
736+ if ( collapseTimeout1Ref . current ) clearTimeout ( collapseTimeout1Ref . current )
737+ if ( collapseTimeout2Ref . current ) clearTimeout ( collapseTimeout2Ref . current )
738+
693739 // After UI updates, ensure code block is visible and update button position
694- setTimeout (
740+ collapseTimeout1Ref . current = setTimeout (
695741 ( ) => {
696- codeBlock . scrollIntoView ( { behavior : "smooth" , block : "nearest" } )
697-
698- // Wait for scroll to complete before updating button position
699- setTimeout ( ( ) => {
700- updateCodeBlockButtonPosition ( )
701- } , 50 )
742+ if ( codeBlock ) {
743+ // Check if codeBlock element still exists
744+ codeBlock . scrollIntoView ( { behavior : "smooth" , block : "nearest" } )
745+
746+ // Wait for scroll to complete before updating button position
747+ collapseTimeout2Ref . current = setTimeout ( ( ) => {
748+ // updateCodeBlockButtonPosition itself should also check for refs if needed
749+ updateCodeBlockButtonPosition ( )
750+ collapseTimeout2Ref . current = null
751+ } , 50 )
752+ }
753+ collapseTimeout1Ref . current = null
702754 } ,
703755 WINDOW_SHADE_SETTINGS . transitionDelayS * 1000 + 50 ,
704756 )
0 commit comments