Skip to content

Commit 2b7f2ae

Browse files
authored
fix: support HMR for class components (#320)
1 parent 302a323 commit 2b7f2ae

File tree

14 files changed

+163
-8
lines changed

14 files changed

+163
-8
lines changed

packages/plugin-react/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
### Support HMR for class components
6+
7+
This is a long overdue and should fix some issues people had with HMR when migrating from CRA.
8+
59
## 4.2.1 (2023-12-04)
610

711
Remove generic parameter on `Plugin` to avoid type error with Rollup 4/Vite 5 and `skipLibCheck: false`.

packages/plugin-react/src/fast-refresh.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,12 @@ window.$RefreshSig$ = () => (type) => type
2828
window.__vite_plugin_react_preamble_installed__ = true
2929
`
3030

31-
const header = `
31+
const sharedHeader = `
3232
import RefreshRuntime from "${runtimePublicPath}";
3333
3434
const inWebWorker = typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope;
35+
`.replace(/\n+/g, '')
36+
const functionHeader = `
3537
let prevRefreshReg;
3638
let prevRefreshSig;
3739
@@ -51,11 +53,13 @@ if (import.meta.hot && !inWebWorker) {
5153
window.$RefreshSig$ = RefreshRuntime.createSignatureFunctionForTransform;
5254
}`.replace(/\n+/g, '')
5355

54-
const footer = `
56+
const functionFooter = `
5557
if (import.meta.hot && !inWebWorker) {
5658
window.$RefreshReg$ = prevRefreshReg;
5759
window.$RefreshSig$ = prevRefreshSig;
58-
60+
}`
61+
const sharedFooter = `
62+
if (import.meta.hot && !inWebWorker) {
5963
RefreshRuntime.__hmr_import(import.meta.url).then((currentExports) => {
6064
RefreshRuntime.registerExportsForReactRefresh(__SOURCE__, currentExports);
6165
import.meta.hot.accept((nextExports) => {
@@ -68,8 +72,19 @@ if (import.meta.hot && !inWebWorker) {
6872

6973
export function addRefreshWrapper(code: string, id: string): string {
7074
return (
71-
header.replace('__SOURCE__', JSON.stringify(id)) +
75+
sharedHeader +
76+
functionHeader.replace('__SOURCE__', JSON.stringify(id)) +
7277
code +
73-
footer.replace('__SOURCE__', JSON.stringify(id))
78+
functionFooter +
79+
sharedFooter.replace('__SOURCE__', JSON.stringify(id))
80+
)
81+
}
82+
83+
export function addClassComponentRefreshWrapper(
84+
code: string,
85+
id: string,
86+
): string {
87+
return (
88+
sharedHeader + code + sharedFooter.replace('__SOURCE__', JSON.stringify(id))
7489
)
7590
}

packages/plugin-react/src/index.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type {
1111
UserConfig,
1212
} from 'vite'
1313
import {
14+
addClassComponentRefreshWrapper,
1415
addRefreshWrapper,
1516
preambleCode,
1617
runtimeCode,
@@ -86,6 +87,7 @@ export type ViteReactPluginApi = {
8687
reactBabel?: ReactBabelHook
8788
}
8889

90+
const reactCompRE = /extends\s+(?:React\.)?(?:Pure)?Component/
8991
const refreshContentRE = /\$Refresh(?:Reg|Sig)\$\(/
9092
const defaultIncludeRE = /\.[tj]sx?$/
9193
const tsRE = /\.tsx?$/
@@ -250,8 +252,12 @@ export default function viteReact(opts: Options = {}): PluginOption[] {
250252

251253
if (result) {
252254
let code = result.code!
253-
if (useFastRefresh && refreshContentRE.test(code)) {
254-
code = addRefreshWrapper(code, id)
255+
if (useFastRefresh) {
256+
if (refreshContentRE.test(code)) {
257+
code = addRefreshWrapper(code, id)
258+
} else if (reactCompRE.test(code)) {
259+
code = addClassComponentRefreshWrapper(code, id)
260+
}
255261
}
256262
return { code, map: result.map }
257263
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { expect, test } from 'vitest'
2+
import {
3+
editFile,
4+
isServe,
5+
page,
6+
untilBrowserLogAfter,
7+
untilUpdated,
8+
} from '~utils'
9+
10+
test('should render', async () => {
11+
expect(await page.textContent('span')).toMatch('Hello World')
12+
})
13+
14+
if (isServe) {
15+
test('Class component HMR', async () => {
16+
editFile('src/App.tsx', (code) => code.replace('World', 'class components'))
17+
await untilBrowserLogAfter(
18+
() => page.textContent('span'),
19+
'[vite] hot updated: /src/App.tsx',
20+
)
21+
await untilUpdated(() => page.textContent('span'), 'Hello class components')
22+
23+
editFile('src/utils.tsx', (code) => code.replace('Hello', 'Hi'))
24+
await untilBrowserLogAfter(
25+
() => page.textContent('span'),
26+
'[vite] hot updated: /src/App.tsx',
27+
)
28+
await untilUpdated(() => page.textContent('span'), 'Hi class components')
29+
})
30+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<title>Vite + React + class components</title>
8+
</head>
9+
<body>
10+
<div id="root"></div>
11+
<script type="module" src="/src/index.tsx"></script>
12+
</body>
13+
</html>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "@vitejs/test-class-components",
3+
"private": true,
4+
"type": "module",
5+
"scripts": {
6+
"dev": "vite",
7+
"build": "vite build",
8+
"preview": "vite preview"
9+
},
10+
"dependencies": {
11+
"react": "^18.3.1",
12+
"react-dom": "^18.3.1"
13+
},
14+
"devDependencies": {
15+
"@types/react": "^18.3.2",
16+
"@types/react-dom": "^18.3.0",
17+
"@vitejs/plugin-react": "workspace:*"
18+
}
19+
}
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { Component } from 'react'
2+
import { getGetting } from './utils'
3+
4+
export class App extends Component {
5+
render() {
6+
return <span>{getGetting()} World</span>
7+
}
8+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { StrictMode } from 'react'
2+
import { createRoot } from 'react-dom/client'
3+
import { App } from './App'
4+
5+
createRoot(document.getElementById('root')!).render(
6+
<StrictMode>
7+
<App />
8+
</StrictMode>,
9+
)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const getGetting = () => <span>Hello</span>

0 commit comments

Comments
 (0)