Skip to content

Commit f3e6544

Browse files
authored
refactor(hmr): root component should be hmr friendly (#2033)
1 parent 389938a commit f3e6544

File tree

6 files changed

+157
-149
lines changed

6 files changed

+157
-149
lines changed

packages/core/src/runtime/App.tsx

+3-117
Original file line numberDiff line numberDiff line change
@@ -1,130 +1,16 @@
1-
import {
2-
DataContext,
3-
isEqualPath,
4-
pathnameToRouteService,
5-
useLocation,
6-
} from '@rspress/runtime';
7-
import {
8-
type BaseRuntimePageInfo,
9-
type FrontMatterMeta,
10-
type Header,
11-
MDX_OR_MD_REGEXP,
12-
type PageData,
13-
cleanUrl,
14-
} from '@rspress/shared';
1+
import { DataContext, useLocation } from '@rspress/runtime';
152
import { Layout } from '@theme';
163
import React, { useContext, useLayoutEffect } from 'react';
174
import { HelmetProvider } from 'react-helmet-async';
185
import globalComponents from 'virtual-global-components';
19-
import siteData from 'virtual-site-data';
206
import 'virtual-global-styles';
7+
import { initPageData } from './initPageData';
218

22-
export enum QueryStatus {
9+
enum QueryStatus {
2310
Show = '1',
2411
Hide = '0',
2512
}
2613

27-
type PageMeta = {
28-
title: string;
29-
headingTitle: string;
30-
toc: Header[];
31-
frontmatter: FrontMatterMeta;
32-
};
33-
34-
export async function initPageData(routePath: string): Promise<PageData> {
35-
const matchedRoute = pathnameToRouteService(routePath);
36-
if (matchedRoute) {
37-
// Preload route component
38-
const mod = await matchedRoute.preload();
39-
const pagePath = cleanUrl(matchedRoute.filePath);
40-
const normalize = (p: string) =>
41-
// compat the path that has no / suffix and ignore case
42-
p
43-
.replace(/\/$/, '')
44-
.toLowerCase();
45-
const extractPageInfo: BaseRuntimePageInfo = siteData.pages.find(page =>
46-
isEqualPath(normalize(page.routePath), normalize(matchedRoute.path)),
47-
)!;
48-
49-
// FIXME: when sidebar item is configured as link string, the sidebar text won't updated when page title changed
50-
// Reason: The sidebar item text depends on pageData, which is not updated when page title changed, because the pageData is computed once when build
51-
const encodedPagePath = encodeURIComponent(pagePath);
52-
const meta: PageMeta =
53-
(
54-
mod.default as unknown as {
55-
__RSPRESS_PAGE_META: Record<string, PageMeta>;
56-
}
57-
).__RSPRESS_PAGE_META?.[encodedPagePath] || ({} as PageMeta);
58-
// mdx loader will generate __RSPRESS_PAGE_META,
59-
// if the filePath don't match it, we can get meta from j(t)sx if we customize it
60-
const {
61-
toc = [],
62-
title = '',
63-
frontmatter = {},
64-
...rest
65-
} = MDX_OR_MD_REGEXP.test(matchedRoute.filePath)
66-
? meta
67-
: (mod as unknown as PageMeta);
68-
return {
69-
siteData,
70-
page: {
71-
...rest,
72-
pagePath,
73-
...extractPageInfo,
74-
pageType: frontmatter?.pageType || 'doc',
75-
title,
76-
frontmatter,
77-
toc,
78-
},
79-
} satisfies PageData;
80-
}
81-
82-
let lang = siteData.lang || '';
83-
let version = siteData.multiVersion?.default || '';
84-
85-
if (siteData.lang && typeof window !== 'undefined') {
86-
const path = location.pathname
87-
.replace(siteData.base, '')
88-
.split('/')
89-
.slice(0, 2);
90-
91-
if (siteData.locales.length) {
92-
const result = siteData.locales.find(({ lang }) => path.includes(lang));
93-
94-
if (result) {
95-
lang = result.lang;
96-
}
97-
}
98-
99-
if (siteData.multiVersion.versions) {
100-
const result = siteData.multiVersion.versions.find(version =>
101-
path.includes(version),
102-
);
103-
104-
if (result) {
105-
version = result;
106-
}
107-
}
108-
}
109-
110-
// 404 Page
111-
return {
112-
siteData,
113-
page: {
114-
pagePath: '',
115-
pageType: '404',
116-
routePath: '/404',
117-
lang,
118-
frontmatter: {},
119-
title: '404',
120-
toc: [],
121-
version,
122-
_filepath: '',
123-
_relativePath: '',
124-
},
125-
};
126-
}
127-
12814
export function App({ helmetContext }: { helmetContext?: object }) {
12915
const { setData: setPageData, data } = useContext(DataContext);
13016
const frontmatter = data.page.frontmatter || {};
+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { BrowserRouter, DataContext, ThemeContext } from '@rspress/runtime';
2+
import type { PageData } from '@rspress/shared';
3+
import { useThemeState } from '@theme';
4+
import { useEffect, useMemo, useState } from 'react';
5+
import { App } from './App';
6+
import { initPageData } from './initPageData';
7+
8+
export function ClientApp() {
9+
const [data, setData] = useState<PageData>(null as any);
10+
const [theme, setTheme] = useThemeState();
11+
12+
useEffect(() => {
13+
initPageData(window.location.pathname).then(pageData => {
14+
setData(pageData);
15+
});
16+
}, []);
17+
18+
const themeValue = useMemo(() => ({ theme, setTheme }), [theme, setTheme]);
19+
const dataValue = useMemo(() => ({ data, setData }), [data, setData]);
20+
21+
if (!data) {
22+
return <></>;
23+
}
24+
25+
return (
26+
<ThemeContext.Provider value={themeValue}>
27+
<DataContext.Provider value={dataValue}>
28+
<BrowserRouter>
29+
<App />
30+
</BrowserRouter>
31+
</DataContext.Provider>
32+
</ThemeContext.Provider>
33+
);
34+
}

packages/core/src/runtime/clientEntry.tsx

+5-30
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
1-
import { BrowserRouter, DataContext, ThemeContext } from '@rspress/runtime';
21
import { isProduction } from '@rspress/shared';
3-
import { useThemeState } from '@theme';
4-
import { useMemo, useState } from 'react';
52
import siteData from 'virtual-site-data';
6-
import { App, initPageData } from './App';
3+
import { ClientApp } from './ClientApp';
74

85
const enableSSG = siteData.ssg;
96

@@ -12,41 +9,19 @@ const enableSSG = siteData.ssg;
129
export async function renderInBrowser() {
1310
const container = document.getElementById('root')!;
1411

15-
const enhancedApp = async () => {
16-
const initialPageData = await initPageData(window.location.pathname);
17-
18-
return function RootApp() {
19-
const [data, setData] = useState(initialPageData);
20-
const [theme, setTheme] = useThemeState();
21-
return (
22-
<ThemeContext.Provider
23-
value={useMemo(() => ({ theme, setTheme }), [theme, setTheme])}
24-
>
25-
<DataContext.Provider
26-
value={useMemo(() => ({ data, setData }), [data, setData])}
27-
>
28-
<BrowserRouter>
29-
<App />
30-
</BrowserRouter>
31-
</DataContext.Provider>
32-
</ThemeContext.Provider>
33-
);
34-
};
35-
};
36-
const RootApp = await enhancedApp();
3712
if (process.env.__REACT_GTE_18__) {
3813
const { createRoot, hydrateRoot } = require('react-dom/client');
3914
if (isProduction() && enableSSG) {
40-
hydrateRoot(container, <RootApp />);
15+
hydrateRoot(container, <ClientApp />);
4116
} else {
42-
createRoot(container).render(<RootApp />);
17+
createRoot(container).render(<ClientApp />);
4318
}
4419
} else {
4520
const ReactDOM = require('react-dom');
4621
if (isProduction()) {
47-
ReactDOM.hydrate(<RootApp />, container);
22+
ReactDOM.hydrate(<ClientApp />, container);
4823
} else {
49-
ReactDOM.render(<RootApp />, container);
24+
ReactDOM.render(<ClientApp />, container);
5025
}
5126
}
5227
}
+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { isEqualPath, pathnameToRouteService } from '@rspress/runtime';
2+
import {
3+
type BaseRuntimePageInfo,
4+
type FrontMatterMeta,
5+
type Header,
6+
MDX_OR_MD_REGEXP,
7+
type PageData,
8+
cleanUrl,
9+
} from '@rspress/shared';
10+
import 'virtual-global-styles';
11+
import siteData from 'virtual-site-data';
12+
13+
type PageMeta = {
14+
title: string;
15+
headingTitle: string;
16+
toc: Header[];
17+
frontmatter: FrontMatterMeta;
18+
};
19+
20+
export async function initPageData(routePath: string): Promise<PageData> {
21+
const matchedRoute = pathnameToRouteService(routePath);
22+
if (matchedRoute) {
23+
// Preload route component
24+
const mod = await matchedRoute.preload();
25+
const pagePath = cleanUrl(matchedRoute.filePath);
26+
const normalize = (p: string) =>
27+
// compat the path that has no / suffix and ignore case
28+
p
29+
.replace(/\/$/, '')
30+
.toLowerCase();
31+
const extractPageInfo: BaseRuntimePageInfo = siteData.pages.find(page =>
32+
isEqualPath(normalize(page.routePath), normalize(matchedRoute.path)),
33+
)!;
34+
35+
// FIXME: when sidebar item is configured as link string, the sidebar text won't updated when page title changed
36+
// Reason: The sidebar item text depends on pageData, which is not updated when page title changed, because the pageData is computed once when build
37+
const encodedPagePath = encodeURIComponent(pagePath);
38+
const meta: PageMeta =
39+
(
40+
mod.default as unknown as {
41+
__RSPRESS_PAGE_META: Record<string, PageMeta>;
42+
}
43+
).__RSPRESS_PAGE_META?.[encodedPagePath] || ({} as PageMeta);
44+
// mdx loader will generate __RSPRESS_PAGE_META,
45+
// if the filePath don't match it, we can get meta from j(t)sx if we customize it
46+
const {
47+
toc = [],
48+
title = '',
49+
frontmatter = {},
50+
...rest
51+
} = MDX_OR_MD_REGEXP.test(matchedRoute.filePath)
52+
? meta
53+
: (mod as unknown as PageMeta);
54+
return {
55+
siteData,
56+
page: {
57+
...rest,
58+
pagePath,
59+
...extractPageInfo,
60+
pageType: frontmatter?.pageType || 'doc',
61+
title,
62+
frontmatter,
63+
toc,
64+
},
65+
} satisfies PageData;
66+
}
67+
68+
let lang = siteData.lang || '';
69+
let version = siteData.multiVersion?.default || '';
70+
71+
if (siteData.lang && typeof window !== 'undefined') {
72+
const path = location.pathname
73+
.replace(siteData.base, '')
74+
.split('/')
75+
.slice(0, 2);
76+
77+
if (siteData.locales.length) {
78+
const result = siteData.locales.find(({ lang }) => path.includes(lang));
79+
80+
if (result) {
81+
lang = result.lang;
82+
}
83+
}
84+
85+
if (siteData.multiVersion.versions) {
86+
const result = siteData.multiVersion.versions.find(version =>
87+
path.includes(version),
88+
);
89+
90+
if (result) {
91+
version = result;
92+
}
93+
}
94+
}
95+
96+
// 404 Page
97+
return {
98+
siteData,
99+
page: {
100+
pagePath: '',
101+
pageType: '404',
102+
routePath: '/404',
103+
lang,
104+
frontmatter: {},
105+
title: '404',
106+
toc: [],
107+
version,
108+
_filepath: '',
109+
_relativePath: '',
110+
},
111+
};
112+
}

packages/core/src/runtime/ssrEntry.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { DataContext, ThemeContext } from '@rspress/runtime';
22
import { StaticRouter } from '@rspress/runtime/server';
33
import type { PageData } from '@rspress/shared';
44
import { renderToString } from 'react-dom/server';
5-
import { App, initPageData } from './App';
5+
import { App } from './App';
6+
import { initPageData } from './initPageData';
67

78
const DEFAULT_THEME = 'light';
89

packages/theme-default/src/logic/useUISwitch.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useEffect, useState } from 'react';
44
import { useEnableNav, useHiddenNav } from './useHiddenNav';
55
import { useLocaleSiteData } from './useLocaleSiteData';
66

7-
export enum QueryStatus {
7+
enum QueryStatus {
88
Show = '1',
99
Hide = '0',
1010
}

0 commit comments

Comments
 (0)