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 }