1
1
'use client' ;
2
2
3
3
import type { DocumentBlockCode } from '@gitbook/api' ;
4
- import { useEffect , useRef , useState } from 'react' ;
4
+ import { useEffect , useMemo , useRef , useState } from 'react' ;
5
5
6
6
import { useInViewportListener } from '@/components/hooks/useInViewportListener' ;
7
- import { useScrollListener } from '@/components/hooks/useScrollListener' ;
8
- import { useDebounceCallback , useEventCallback } from 'usehooks-ts' ;
9
7
import type { BlockProps } from '../Block' ;
10
8
import { CodeBlockRenderer } from './CodeBlockRenderer' ;
11
9
import type { HighlightLine , RenderedInline } from './highlight' ;
@@ -22,53 +20,56 @@ type ClientBlockProps = Pick<BlockProps<DocumentBlockCode>, 'block' | 'style'> &
22
20
export function ClientCodeBlock ( props : ClientBlockProps ) {
23
21
const { block, style, inlines } = props ;
24
22
const blockRef = useRef < HTMLDivElement > ( null ) ;
25
- const processedRef = useRef ( false ) ;
26
- const isInViewportRef = useRef < boolean | null > ( null ) ;
27
- const [ lines , setLines ] = useState < HighlightLine [ ] > ( ( ) => plainHighlight ( block , [ ] ) ) ;
23
+ const [ isInViewport , setIsInViewport ] = useState ( false ) ;
24
+ const plainLines = useMemo ( ( ) => plainHighlight ( block , [ ] ) , [ block ] ) ;
25
+ const [ lines , setLines ] = useState < null | HighlightLine [ ] > ( null ) ;
28
26
29
27
// Preload the highlighter when the block is mounted.
30
28
useEffect ( ( ) => {
31
29
import ( './highlight' ) . then ( ( { preloadHighlight } ) => preloadHighlight ( block ) ) ;
32
30
} , [ block ] ) ;
33
31
34
- const runHighlight = useEventCallback ( ( ) => {
35
- if ( processedRef . current ) {
36
- return ;
37
- }
38
- if ( typeof window !== 'undefined' ) {
39
- import ( './highlight' ) . then ( ( { highlight } ) => {
40
- highlight ( block , inlines ) . then ( ( lines ) => {
41
- setLines ( lines ) ;
42
- processedRef . current = true ;
43
- } ) ;
44
- } ) ;
45
- }
46
- } ) ;
47
- const debouncedRunHighlight = useDebounceCallback ( runHighlight , 1000 ) ;
48
-
32
+ // Detect when the block is in viewport
49
33
useInViewportListener (
50
34
blockRef ,
51
35
( isInViewport , disconnect ) => {
52
- // Disconnect once in viewport
53
36
if ( isInViewport ) {
37
+ // Disconnect once in viewport
54
38
disconnect ( ) ;
55
- // If it's initially in viewport, we need to run the highlight
56
- if ( isInViewportRef . current === null ) {
57
- runHighlight ( ) ;
58
- }
39
+ setIsInViewport ( true ) ;
59
40
}
60
- isInViewportRef . current = isInViewport ;
61
41
} ,
62
42
{ rootMargin : '200px' }
63
43
) ;
64
44
65
- const handleScroll = useDebounceCallback ( ( ) => {
66
- if ( isInViewportRef . current ) {
67
- debouncedRunHighlight ( ) ;
45
+ // When the block is visible or updated, we need to re-run the highlight
46
+ useEffect ( ( ) => {
47
+ if ( isInViewport ) {
48
+ // If the block is in viewport, we need to run the highlight
49
+ let cancelled = false ;
50
+
51
+ if ( typeof window !== 'undefined' ) {
52
+ import ( './highlight' ) . then ( ( { highlight } ) => {
53
+ highlight ( block , inlines ) . then ( ( lines ) => {
54
+ if ( cancelled ) {
55
+ return ;
56
+ }
57
+
58
+ setLines ( lines ) ;
59
+ } ) ;
60
+ } ) ;
61
+ }
62
+
63
+ return ( ) => {
64
+ cancelled = true ;
65
+ } ;
68
66
}
69
- } , 80 ) ;
70
67
71
- useScrollListener ( handleScroll , useRef ( typeof window !== 'undefined' ? window : null ) ) ;
68
+ // Otherwise if the block is not in viewport, we reset to the plain lines
69
+ setLines ( null ) ;
70
+ } , [ isInViewport , block , inlines ] ) ;
72
71
73
- return < CodeBlockRenderer ref = { blockRef } block = { block } style = { style } lines = { lines } /> ;
72
+ return (
73
+ < CodeBlockRenderer ref = { blockRef } block = { block } style = { style } lines = { lines ?? plainLines } />
74
+ ) ;
74
75
}
0 commit comments