Skip to content

Commit ace6190

Browse files
conico974Nicolas Dorseuil
andauthored
Fix ScrollSectionsList with suspended component (#3390)
Co-authored-by: Nicolas Dorseuil <[email protected]>
1 parent e6c3c76 commit ace6190

File tree

8 files changed

+76
-18
lines changed

8 files changed

+76
-18
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/PDF/PageControlButtons.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export function PageControlButtons(props: {
3232
}, [pageIds]);
3333
const activeDivId = useScrollActiveId(divIds, {
3434
threshold: 0,
35+
enabled: true,
3536
});
3637
const activeIndex = (activeDivId ? divIds.indexOf(activeDivId) : 0) + 1;
3738
const activePageId = pageIds[activeIndex - 1]?.[0];

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

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

9+
import { useBodyLoaded } from '@/components/primitives';
910
import { HEADER_HEIGHT_DESKTOP } from '../layout';
1011
import { AsideSectionHighlight } from './AsideSectionHighlight';
1112

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

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

3842
return (

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { DocumentView, DocumentViewSkeleton } from '../DocumentView';
1111
import { TrackPageViewEvent } from '../Insights';
1212
import { PageFeedbackForm } from '../PageFeedback';
1313
import { CurrentPageProvider } from '../hooks/useCurrentPage';
14-
import { DateRelative } from '../primitives';
14+
import { DateRelative, SuspenseLoadedHint } from '../primitives';
1515
import { PageBodyBlankslate } from './PageBodyBlankslate';
1616
import { PageCover } from './PageCover';
1717
import { PageFooterNavigation } from './PageFooterNavigation';
@@ -73,6 +73,7 @@ export function PageBody(props: {
7373
/>
7474
}
7575
>
76+
<SuspenseLoadedHint />
7677
<DocumentView
7778
document={document}
7879
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: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,20 @@ export function useScrollActiveId(
88
options: {
99
rootMargin?: string;
1010
threshold?: number;
11-
} = {}
11+
enabled: boolean;
12+
} = { enabled: true }
1213
) {
13-
const { rootMargin, threshold = 0.5 } = options;
14+
const { rootMargin, threshold = 0.5, enabled } = options;
1415

1516
const [activeId, setActiveId] = React.useState<string>(ids[0]);
1617
const sectionsIntersectingMap = React.useRef<Map<string, boolean>>(new Map());
1718

1819
React.useEffect(() => {
1920
const defaultActiveId = ids[0];
2021
setActiveId((activeId) => (ids.indexOf(activeId) !== -1 ? activeId : defaultActiveId));
22+
if (!enabled) {
23+
return;
24+
}
2125

2226
if (typeof IntersectionObserver === 'undefined') {
2327
return;
@@ -70,7 +74,7 @@ export function useScrollActiveId(
7074
return () => {
7175
observer.disconnect();
7276
};
73-
}, [ids, threshold, rootMargin]);
77+
}, [ids, threshold, rootMargin, enabled]);
7478

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

packages/gitbook/src/components/primitives/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ export * from './StyledLink';
88
export * from './DateRelative';
99
export * from './Emoji';
1010
export * from './LoadingPane';
11+
export * from './LoadingStateProvider';

0 commit comments

Comments
 (0)