diff --git a/.changeset/calm-bats-create.md b/.changeset/calm-bats-create.md new file mode 100644 index 000000000000..3d11c5ec15dc --- /dev/null +++ b/.changeset/calm-bats-create.md @@ -0,0 +1,5 @@ +--- +'@astrojs/preact': patch +--- + +Fixed an issue where the Preact integration would incorrectly intercept React 19 components, triggering "Invalid hook call" error logs. diff --git a/packages/astro/e2e/fixtures/react19-preact-hook-error/astro.config.mjs b/packages/astro/e2e/fixtures/react19-preact-hook-error/astro.config.mjs new file mode 100644 index 000000000000..7640254b068b --- /dev/null +++ b/packages/astro/e2e/fixtures/react19-preact-hook-error/astro.config.mjs @@ -0,0 +1,12 @@ +import preact from '@astrojs/preact'; +import react from '@astrojs/react'; +import { defineConfig } from 'astro/config'; + +// https://astro.build/config +export default defineConfig({ + integrations: [ + // This issue only reproduces when the Preact integration is placed before the React integration. + preact({ include: ['**/preact/*'] }), + react({ include: ['**/react/*'] }), + ], +}); diff --git a/packages/astro/e2e/fixtures/react19-preact-hook-error/package.json b/packages/astro/e2e/fixtures/react19-preact-hook-error/package.json new file mode 100644 index 000000000000..3b1fc64a4d6e --- /dev/null +++ b/packages/astro/e2e/fixtures/react19-preact-hook-error/package.json @@ -0,0 +1,15 @@ +{ + "name": "@e2e/react19-preact-hook-error", + "version": "0.0.0", + "private": true, + "devDependencies": { + "@astrojs/preact": "workspace:*", + "@astrojs/react": "workspace:*", + "astro": "workspace:*" + }, + "dependencies": { + "preact": "^10.28.3", + "react": "^19.2.4", + "react-dom": "^19.2.4" + } +} diff --git a/packages/astro/e2e/fixtures/react19-preact-hook-error/src/components/preact/PreactCounter.tsx b/packages/astro/e2e/fixtures/react19-preact-hook-error/src/components/preact/PreactCounter.tsx new file mode 100644 index 000000000000..ace9c01ba428 --- /dev/null +++ b/packages/astro/e2e/fixtures/react19-preact-hook-error/src/components/preact/PreactCounter.tsx @@ -0,0 +1,20 @@ +import type { ComponentChildren } from 'preact'; +import { useState } from 'preact/hooks'; + +/** A counter written with Preact */ +export function PreactCounter({ children }: { children?: ComponentChildren }) { + const [count, setCount] = useState(0); + const add = () => setCount((i) => i + 1); + const subtract = () => setCount((i) => i - 1); + + return ( + <> +
+ +
{count}
+ +
+
{children}
+ + ); +} diff --git a/packages/astro/e2e/fixtures/react19-preact-hook-error/src/components/react/ReactCounter.tsx b/packages/astro/e2e/fixtures/react19-preact-hook-error/src/components/react/ReactCounter.tsx new file mode 100644 index 000000000000..02eb1953907d --- /dev/null +++ b/packages/astro/e2e/fixtures/react19-preact-hook-error/src/components/react/ReactCounter.tsx @@ -0,0 +1,19 @@ +import { useState } from 'react'; + +/** a counter written in React */ +export function Counter({ children, id }) { + const [count, setCount] = useState(0); + const add = () => setCount((i) => i + 1); + const subtract = () => setCount((i) => i - 1); + + return ( + <> +
+ +
{count}
+ +
+
{children}
+ + ); +} diff --git a/packages/astro/e2e/fixtures/react19-preact-hook-error/src/pages/index.astro b/packages/astro/e2e/fixtures/react19-preact-hook-error/src/pages/index.astro new file mode 100644 index 000000000000..7dfd9f20de49 --- /dev/null +++ b/packages/astro/e2e/fixtures/react19-preact-hook-error/src/pages/index.astro @@ -0,0 +1,28 @@ +--- +// Style Imports +import '../styles/global.css'; + +import { PreactCounter } from '../components/preact/PreactCounter'; +import * as react from '../components/react/ReactCounter'; +--- + + + + + + + + + + +
+ +

Hello from React!

+
+ + +

Hello from Preact!

+
+
+ + diff --git a/packages/astro/e2e/fixtures/react19-preact-hook-error/src/styles/global.css b/packages/astro/e2e/fixtures/react19-preact-hook-error/src/styles/global.css new file mode 100644 index 000000000000..4912b4c399a4 --- /dev/null +++ b/packages/astro/e2e/fixtures/react19-preact-hook-error/src/styles/global.css @@ -0,0 +1,21 @@ +html, +body { + font-family: system-ui; + margin: 0; +} + +body { + padding: 2rem; +} + +.counter { + display: grid; + font-size: 2em; + grid-template-columns: repeat(3, minmax(0, 1fr)); + margin-top: 2em; + place-items: center; +} + +.counter-message { + text-align: center; +} diff --git a/packages/astro/e2e/react19-preact-hook-error.test.js b/packages/astro/e2e/react19-preact-hook-error.test.js new file mode 100644 index 000000000000..3a28375cbcec --- /dev/null +++ b/packages/astro/e2e/react19-preact-hook-error.test.js @@ -0,0 +1,42 @@ +import { expect } from '@playwright/test'; +import { testFactory } from './test-utils.js'; + +const test = testFactory(import.meta.url, { root: './fixtures/react19-preact-hook-error/' }); + +function hookError() { + const error = console.error; + const errors = []; + console.error = function (...args) { + errors.push(args); + }; + return () => { + console.error = error; + return errors; + }; +} + +let devServer; +let unhook; + +test.beforeAll(async ({ astro }) => { + devServer = await astro.startDevServer(); + unhook = hookError(); +}); + +test.afterAll(async () => { + await devServer.stop(); +}); + +// See: https://github.com/withastro/astro/issues/15341 +test.describe('React v19 and preact hook issue', () => { + test('should not have "Invalid hook call" errors', async ({ page, astro }) => { + await page.goto(astro.resolveUrl('/')); + + const errors = unhook(); + const hasInvalidHookCallErrorLog = errors + .flat() + .some((log) => log.includes('Invalid hook call')); + + expect(hasInvalidHookCallErrorLog).toBe(false); + }); +}); diff --git a/packages/integrations/preact/src/server.ts b/packages/integrations/preact/src/server.ts index fde181343c75..193bb87f6600 100644 --- a/packages/integrations/preact/src/server.ts +++ b/packages/integrations/preact/src/server.ts @@ -136,8 +136,11 @@ function filteredConsoleError(msg: string, ...rest: any[]) { // When attempting this on a React component, React may output // the following error, which we can safely filter out: const isKnownReactHookError = - msg.includes('Warning: Invalid hook call.') && - msg.includes('https://reactjs.org/link/invalid-hook-call'); + msg.includes('Invalid hook call.') && + // for React v18 and earlier + (msg.includes('https://reactjs.org/link/invalid-hook-call') || + // for React v19 and later + msg.includes('https://react.dev/link/invalid-hook-call')); if (isKnownReactHookError) return; } originalConsoleError(msg, ...rest); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 11e30a5c919c..aff0187ef242 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1558,6 +1558,28 @@ importers: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) + packages/astro/e2e/fixtures/react19-preact-hook-error: + dependencies: + preact: + specifier: ^10.28.3 + version: 10.28.4 + react: + specifier: ^19.2.4 + version: 19.2.4 + react-dom: + specifier: ^19.2.4 + version: 19.2.4(react@19.2.4) + devDependencies: + '@astrojs/preact': + specifier: workspace:* + version: link:../../../../integrations/preact + '@astrojs/react': + specifier: workspace:* + version: link:../../../../integrations/react + astro: + specifier: workspace:* + version: link:../../.. + packages/astro/e2e/fixtures/server-islands: dependencies: '@astrojs/mdx':