Skip to content

Commit 5f3f7e8

Browse files
committed
fix ScrollSectionsList not working with suspended component
1 parent 4a295e6 commit 5f3f7e8

File tree

6 files changed

+67
-16
lines changed

6 files changed

+67
-16
lines changed

packages/gitbook/src/components/DocumentView/Block.tsx

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import type { DocumentBlock, JSONDocument } from '@gitbook/api';
2-
import React from 'react';
32

43
import {
54
SkeletonCard,
@@ -47,7 +46,7 @@ export interface BlockProps<Block extends DocumentBlock> extends DocumentContext
4746
}
4847

4948
export function Block<T extends DocumentBlock>(props: BlockProps<T>) {
50-
const { block, style, isEstimatedOffscreen, context } = props;
49+
const { block } = props;
5150

5251
const content = (() => {
5352
switch (block.type) {
@@ -116,17 +115,7 @@ export function Block<T extends DocumentBlock>(props: BlockProps<T>) {
116115
}
117116
})();
118117

119-
if (!isEstimatedOffscreen || context.wrapBlocksInSuspense === false) {
120-
// When blocks are estimated to be on the initial viewport, we render them immediately
121-
// to avoid a flash of a loading skeleton.
122-
return content;
123-
}
124-
125-
return (
126-
<React.Suspense fallback={<BlockSkeleton block={block} style={style} />}>
127-
{content}
128-
</React.Suspense>
129-
);
118+
return content;
130119
}
131120

132121
/**

packages/gitbook/src/components/PageAside/ScrollSectionsList.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { DocumentSection } from '@/lib/document-sections';
77
import { tcls } from '@/lib/tailwind';
88

99
import { HEADER_HEIGHT_DESKTOP } from '../layout';
10+
import { useBodyLoaded } from '../primitives/LoadingStateProvider';
1011
import { AsideSectionHighlight } from './AsideSectionHighlight';
1112

1213
/**
@@ -30,9 +31,12 @@ export function ScrollSectionsList(props: { sections: DocumentSection[] }) {
3031
});
3132
}, [sections]);
3233

34+
const bodyLoaded = useBodyLoaded();
35+
3336
const activeId = useScrollActiveId(ids, {
3437
rootMargin: `-${HEADER_HEIGHT_DESKTOP}px 0px -40% 0px`,
3538
threshold: SECTION_INTERSECTING_THRESHOLD,
39+
additionalEffects: bodyLoaded,
3640
});
3741

3842
return (

packages/gitbook/src/components/PageBody/PageBody.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { DocumentView, DocumentViewSkeleton } from '../DocumentView';
1111
import { TrackPageViewEvent } from '../Insights';
1212
import { PageFeedbackForm } from '../PageFeedback';
1313
import { DateRelative } from '../primitives';
14+
import { SuspenseLoadedHint } from '../primitives/LoadingStateProvider';
1415
import { PageBodyBlankslate } from './PageBodyBlankslate';
1516
import { PageCover } from './PageCover';
1617
import { PageFooterNavigation } from './PageFooterNavigation';
@@ -72,6 +73,7 @@ export function PageBody(props: {
7273
/>
7374
}
7475
>
76+
<SuspenseLoadedHint />
7577
<DocumentView
7678
document={document}
7779
style="grid [&>*+*]:mt-5"

packages/gitbook/src/components/RootLayout/ClientContexts.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,17 @@ import type React from 'react';
44

55
import { TranslateContext } from '@/intl/client';
66
import type { TranslationLanguage } from '@/intl/translations';
7+
import { LoadingStateProvider } from '../primitives/LoadingStateProvider';
78

89
export function ClientContexts(props: {
910
language: TranslationLanguage;
1011
children: React.ReactNode;
1112
}) {
1213
const { children, language } = props;
1314

14-
return <TranslateContext.Provider value={language}>{children}</TranslateContext.Provider>;
15+
return (
16+
<TranslateContext.Provider value={language}>
17+
<LoadingStateProvider>{children}</LoadingStateProvider>
18+
</TranslateContext.Provider>
19+
);
1520
}

packages/gitbook/src/components/hooks/useScrollActiveId.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ export function useScrollActiveId(
88
options: {
99
rootMargin?: string;
1010
threshold?: number;
11+
additionalEffects?: boolean;
1112
} = {}
1213
) {
13-
const { rootMargin, threshold = 0.5 } = options;
14+
const { rootMargin, threshold = 0.5, additionalEffects } = options;
1415

1516
const [activeId, setActiveId] = React.useState<string>(ids[0]);
1617
const sectionsIntersectingMap = React.useRef<Map<string, boolean>>(new Map());
@@ -70,7 +71,7 @@ export function useScrollActiveId(
7071
return () => {
7172
observer.disconnect();
7273
};
73-
}, [ids, threshold, rootMargin]);
74+
}, [ids, threshold, rootMargin, additionalEffects]);
7475

7576
return activeId;
7677
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
'use client';
2+
3+
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
4+
5+
export const LoadingStateProviderContext = createContext<{
6+
bodyLoaded: boolean;
7+
setBodyLoaded: (loaded: boolean) => void;
8+
}>({
9+
bodyLoaded: false,
10+
setBodyLoaded: () => {},
11+
});
12+
13+
/**
14+
* A provider that tracks the loading state of the body.
15+
* This is used to determine when the body has finished loading.
16+
* If we need to track more loading states in the future, we can extend this context.
17+
*/
18+
export function LoadingStateProvider({
19+
children,
20+
}: {
21+
children: React.ReactNode;
22+
}) {
23+
const [bodyLoaded, setBodyLoaded] = useState(false);
24+
25+
return (
26+
<LoadingStateProviderContext.Provider value={{ bodyLoaded, setBodyLoaded }}>
27+
{children}
28+
</LoadingStateProviderContext.Provider>
29+
);
30+
}
31+
32+
/** * Hook to get the current loading state of the body.
33+
* Returns true if the body has finished loading inside the suspense boundary.
34+
*/
35+
export function useBodyLoaded() {
36+
const context = useContext(LoadingStateProviderContext);
37+
return useMemo(() => context.bodyLoaded, [context.bodyLoaded]);
38+
}
39+
40+
/**
41+
* A component that sets the body as loaded when it is mounted.
42+
* It should be used inside a Suspense boundary to indicate that the body has finished loading.
43+
*/
44+
export function SuspenseLoadedHint() {
45+
const context = useContext(LoadingStateProviderContext);
46+
useEffect(() => {
47+
context.setBodyLoaded(true);
48+
}, [context]);
49+
return null;
50+
}

0 commit comments

Comments
 (0)