diff --git a/packages/common/refresh-runtime.js b/packages/common/refresh-runtime.js
index 08d1df630..0029017fe 100644
--- a/packages/common/refresh-runtime.js
+++ b/packages/common/refresh-runtime.js
@@ -545,6 +545,21 @@ function isLikelyComponentType(type) {
}
}
+function isCompoundComponent(type) {
+ if (!isPlainObject(type)) return false
+ for (const key in type) {
+ if (!isLikelyComponentType(type[key])) return false
+ }
+ return true
+}
+
+function isPlainObject(obj) {
+ return (
+ Object.prototype.toString.call(obj) === '[object Object]' &&
+ (obj.constructor === Object || obj.constructor === undefined)
+ )
+}
+
/**
* Plugin utils
*/
@@ -565,6 +580,13 @@ export function registerExportsForReactRefresh(filename, moduleExports) {
// The register function has an identity check to not register twice the same component,
// so this is safe to not used the same key here.
register(exportValue, filename + ' export ' + key)
+ } else if (isCompoundComponent(exportValue)) {
+ for (const subKey in exportValue) {
+ register(
+ exportValue[subKey],
+ filename + ' export ' + key + '-' + subKey,
+ )
+ }
}
}
}
@@ -618,6 +640,7 @@ export function validateRefreshBoundaryAndEnqueueUpdate(
(key, value) => {
hasExports = true
if (isLikelyComponentType(value)) return true
+ if (isCompoundComponent(value)) return true
return prevExports[key] === nextExports[key]
},
)
diff --git a/packages/plugin-react-oxc/CHANGELOG.md b/packages/plugin-react-oxc/CHANGELOG.md
index e82a2fa30..146a51778 100644
--- a/packages/plugin-react-oxc/CHANGELOG.md
+++ b/packages/plugin-react-oxc/CHANGELOG.md
@@ -2,6 +2,17 @@
## Unreleased
+### Add HMR support for compound components ([#518](https://github.com/vitejs/vite-plugin-react/pull/518))
+
+HMR now works for compound components like this:
+
+```tsx
+const Root = () =>
Accordion Root
+const Item = () => Accordion Item
+
+export const Accordion = { Root, Item }
+```
+
### Return `Plugin[]` instead of `PluginOption[]`
## 0.2.3 (2025-06-16)
diff --git a/packages/plugin-react-swc/CHANGELOG.md b/packages/plugin-react-swc/CHANGELOG.md
index 5a2b0dd8e..a23f92d0a 100644
--- a/packages/plugin-react-swc/CHANGELOG.md
+++ b/packages/plugin-react-swc/CHANGELOG.md
@@ -2,6 +2,17 @@
## Unreleased
+### Add HMR support for compound components ([#518](https://github.com/vitejs/vite-plugin-react/pull/518))
+
+HMR now works for compound components like this:
+
+```tsx
+const Root = () => Accordion Root
+const Item = () => Accordion Item
+
+export const Accordion = { Root, Item }
+```
+
### Return `Plugin[]` instead of `PluginOption[]`
## 3.10.2 (2025-06-10)
diff --git a/packages/plugin-react/CHANGELOG.md b/packages/plugin-react/CHANGELOG.md
index 960c754ed..22373b8df 100644
--- a/packages/plugin-react/CHANGELOG.md
+++ b/packages/plugin-react/CHANGELOG.md
@@ -2,6 +2,17 @@
## Unreleased
+### Add HMR support for compound components ([#518](https://github.com/vitejs/vite-plugin-react/pull/518))
+
+HMR now works for compound components like this:
+
+```tsx
+const Root = () => Accordion Root
+const Item = () => Accordion Item
+
+export const Accordion = { Root, Item }
+```
+
### Return `Plugin[]` instead of `PluginOption[]`
The return type has changed from `react(): PluginOption[]` to more specialized type `react(): Plugin[]`. This allows for type-safe manipulation of plugins, for example:
diff --git a/playground/react/App.jsx b/playground/react/App.jsx
index c7ffb805a..3d1b29988 100644
--- a/playground/react/App.jsx
+++ b/playground/react/App.jsx
@@ -1,6 +1,7 @@
import { useState } from 'react'
import Button from 'jsx-entry'
import Dummy from './components/Dummy?qs-should-not-break-plugin-react'
+import { Accordion } from './components/Accordion'
import Parent from './hmr/parent'
import { JsxImportRuntime } from './hmr/jsx-import-runtime'
import { CountProvider } from './context/CountProvider'
@@ -38,6 +39,10 @@ function App() {
+
+ First Item
+ Second Item
+
diff --git a/playground/react/__tests__/react.spec.ts b/playground/react/__tests__/react.spec.ts
index f9d4275af..ea78cabfe 100644
--- a/playground/react/__tests__/react.spec.ts
+++ b/playground/react/__tests__/react.spec.ts
@@ -148,4 +148,19 @@ if (!isBuild) {
expect(await page.textContent('#state-button')).toMatch('count is: 1')
})
+
+ // #493
+ test('should hmr compound components', async () => {
+ await untilBrowserLogAfter(
+ () =>
+ editFile('components/Accordion.jsx', (code) =>
+ code.replace('Accordion Root', 'Accordion Root Updated'),
+ ),
+ ['[vite] hot updated: /components/Accordion.jsx'],
+ )
+
+ await expect
+ .poll(() => page.textContent('#accordion-root'))
+ .toMatch('Accordion Root Updated')
+ })
}
diff --git a/playground/react/components/Accordion.jsx b/playground/react/components/Accordion.jsx
new file mode 100644
index 000000000..c690a48e0
--- /dev/null
+++ b/playground/react/components/Accordion.jsx
@@ -0,0 +1,37 @@
+function Root({ children }) {
+ return (
+
+
+ Accordion Root
+
+
{children}
+
+ )
+}
+
+function Item({ children }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export const Accordion = { Root, Item }