Skip to content

Commit fdc9d9a

Browse files
feat: add hmr support for compound components (#518)
Co-authored-by: Arnaud Barré <[email protected]>
1 parent df6a38e commit fdc9d9a

File tree

7 files changed

+113
-0
lines changed

7 files changed

+113
-0
lines changed

packages/common/refresh-runtime.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,21 @@ function isLikelyComponentType(type) {
545545
}
546546
}
547547

548+
function isCompoundComponent(type) {
549+
if (!isPlainObject(type)) return false
550+
for (const key in type) {
551+
if (!isLikelyComponentType(type[key])) return false
552+
}
553+
return true
554+
}
555+
556+
function isPlainObject(obj) {
557+
return (
558+
Object.prototype.toString.call(obj) === '[object Object]' &&
559+
(obj.constructor === Object || obj.constructor === undefined)
560+
)
561+
}
562+
548563
/**
549564
* Plugin utils
550565
*/
@@ -565,6 +580,13 @@ export function registerExportsForReactRefresh(filename, moduleExports) {
565580
// The register function has an identity check to not register twice the same component,
566581
// so this is safe to not used the same key here.
567582
register(exportValue, filename + ' export ' + key)
583+
} else if (isCompoundComponent(exportValue)) {
584+
for (const subKey in exportValue) {
585+
register(
586+
exportValue[subKey],
587+
filename + ' export ' + key + '-' + subKey,
588+
)
589+
}
568590
}
569591
}
570592
}
@@ -618,6 +640,7 @@ export function validateRefreshBoundaryAndEnqueueUpdate(
618640
(key, value) => {
619641
hasExports = true
620642
if (isLikelyComponentType(value)) return true
643+
if (isCompoundComponent(value)) return true
621644
return prevExports[key] === nextExports[key]
622645
},
623646
)

packages/plugin-react-oxc/CHANGELOG.md

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

33
## Unreleased
44

5+
### Add HMR support for compound components ([#518](https://github.com/vitejs/vite-plugin-react/pull/518))
6+
7+
HMR now works for compound components like this:
8+
9+
```tsx
10+
const Root = () => <div>Accordion Root</div>
11+
const Item = () => <div>Accordion Item</div>
12+
13+
export const Accordion = { Root, Item }
14+
```
15+
516
### Return `Plugin[]` instead of `PluginOption[]`
617

718
## 0.2.3 (2025-06-16)

packages/plugin-react-swc/CHANGELOG.md

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

33
## Unreleased
44

5+
### Add HMR support for compound components ([#518](https://github.com/vitejs/vite-plugin-react/pull/518))
6+
7+
HMR now works for compound components like this:
8+
9+
```tsx
10+
const Root = () => <div>Accordion Root</div>
11+
const Item = () => <div>Accordion Item</div>
12+
13+
export const Accordion = { Root, Item }
14+
```
15+
516
### Return `Plugin[]` instead of `PluginOption[]`
617

718
## 3.10.2 (2025-06-10)

packages/plugin-react/CHANGELOG.md

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

33
## Unreleased
44

5+
### Add HMR support for compound components ([#518](https://github.com/vitejs/vite-plugin-react/pull/518))
6+
7+
HMR now works for compound components like this:
8+
9+
```tsx
10+
const Root = () => <div>Accordion Root</div>
11+
const Item = () => <div>Accordion Item</div>
12+
13+
export const Accordion = { Root, Item }
14+
```
15+
516
### Return `Plugin[]` instead of `PluginOption[]`
617

718
The return type has changed from `react(): PluginOption[]` to more specialized type `react(): Plugin[]`. This allows for type-safe manipulation of plugins, for example:

playground/react/App.jsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useState } from 'react'
22
import Button from 'jsx-entry'
33
import Dummy from './components/Dummy?qs-should-not-break-plugin-react'
4+
import { Accordion } from './components/Accordion'
45
import Parent from './hmr/parent'
56
import { JsxImportRuntime } from './hmr/jsx-import-runtime'
67
import { CountProvider } from './context/CountProvider'
@@ -38,6 +39,10 @@ function App() {
3839
</header>
3940

4041
<Dummy />
42+
<Accordion.Root>
43+
<Accordion.Item>First Item</Accordion.Item>
44+
<Accordion.Item>Second Item</Accordion.Item>
45+
</Accordion.Root>
4146
<Parent />
4247
<JsxImportRuntime />
4348
<Button>button</Button>

playground/react/__tests__/react.spec.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,4 +148,19 @@ if (!isBuild) {
148148

149149
expect(await page.textContent('#state-button')).toMatch('count is: 1')
150150
})
151+
152+
// #493
153+
test('should hmr compound components', async () => {
154+
await untilBrowserLogAfter(
155+
() =>
156+
editFile('components/Accordion.jsx', (code) =>
157+
code.replace('Accordion Root', 'Accordion Root Updated'),
158+
),
159+
['[vite] hot updated: /components/Accordion.jsx'],
160+
)
161+
162+
await expect
163+
.poll(() => page.textContent('#accordion-root'))
164+
.toMatch('Accordion Root Updated')
165+
})
151166
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
function Root({ children }) {
2+
return (
3+
<div
4+
style={{
5+
border: '1px solid #ccc',
6+
borderRadius: '4px',
7+
margin: '16px 0',
8+
}}
9+
>
10+
<h3
11+
id="accordion-root"
12+
style={{ padding: '12px', margin: '0', backgroundColor: '#f5f5f5' }}
13+
>
14+
Accordion Root
15+
</h3>
16+
<div style={{ padding: '12px' }}>{children}</div>
17+
</div>
18+
)
19+
}
20+
21+
function Item({ children }) {
22+
return (
23+
<div
24+
style={{
25+
padding: '8px 12px',
26+
border: '1px solid #e0e0e0',
27+
margin: '4px 0',
28+
borderRadius: '2px',
29+
backgroundColor: '#fafafa',
30+
}}
31+
>
32+
{children}
33+
</div>
34+
)
35+
}
36+
37+
export const Accordion = { Root, Item }

0 commit comments

Comments
 (0)