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