What version of astro are you using?
astro@6.2.1, @astrojs/react@5.0.5, react@19.2.5, react-dom@19.2.5.
Problem
In astro dev, every SSR pass that includes a React island whose top-level function calls any hook (useState, useEffect, useId, useSyncExternalStore, …) emits the React dev-mode warning to the server console:
Invalid hook call. Hooks can only be called inside of the body of a function component.
1. mismatching versions of React and the renderer
2. breaking the Rules of Hooks
3. more than one copy of React in the same app
Hydration succeeds, the page returns 200, unit tests using renderToStaticMarkup pass, and astro build is clean — so it isn't a duplicated-React or rules-of-hooks problem. The warning is purely a side-effect of the renderer probe.
Root cause
packages/integrations/react/src/server.js check() (lines correspond to dist/server.js 26-37 in 5.0.5):
let isReactComponent = false;
function Tester(...args) {
try {
const vnode = Component(...args);
if (vnode && (vnode["$$typeof"] === reactTypeof || vnode["$$typeof"] === reactTransitionalTypeof)) {
isReactComponent = true;
}
} catch {}
return React.createElement("div");
}
await renderToStaticMarkup.call(this, Tester, props, children);
Tester calls Component(...args) directly. When Component runs a hook, React's dev dispatcher fires console.error("Invalid hook call …") because the call did not originate from react-dom/server's normal render path (the dispatcher binding belongs to Tester, not Component). The thrown rules-of-hooks error itself is swallowed by the try/catch, but the console.error is not. The vnode is still returned, $$typeof still matches, isReactComponent becomes true, and hydration proceeds — the warning is cosmetic, but every Astro user with a hook-using island sees it on every dev request.
Reproduction
Minimal: any client:load React component that calls useState at the top level, served via astro dev with @astrojs/react 5.x and React 19.
What I tried (none worked)
vite.ssr.optimizeDeps.include: ['react','react-dom','react/jsx-runtime','react/jsx-dev-runtime']
vite.resolve.dedupe expanded
vite.ssr.resolve.conditions: ['node','import','module','default'] (skip react-server)
@astrojs/react 5.0.4 → 5.0.5
None affected the warning, confirming the source is check(), not module duplication.
Suggested fix
Replace probe-by-invocation with one of:
- Static source probe.
Component.toString() for transpiled markers (React.createElement(, _jsx(, _jsxs(, jsx(, jsxs(). Cheap, no execution.
- Honor
metadata.componentUrl unconditionally. The existing filter branch at line 22 already short-circuits when an explicit opts.include/opts.exclude is set; a metadata.componentUrl ending in .jsx/.tsx (or imported via the React renderer's resolved id) is a reliable positive signal without invocation.
- Hook-safe probe. Temporarily swap
React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE.ReactCurrentDispatcher.current to a no-op throwing dispatcher around the invocation so the rules-of-hooks check sees a "valid" render context. Fragile across React minors — not preferred.
(1) or (2) are clean and don't require touching React internals.
Local investigation thread
bykplx1/qa-learning-site#223 (comment)
What version of
astroare you using?astro@6.2.1,@astrojs/react@5.0.5,react@19.2.5,react-dom@19.2.5.Problem
In
astro dev, every SSR pass that includes a React island whose top-level function calls any hook (useState,useEffect,useId,useSyncExternalStore, …) emits the React dev-mode warning to the server console:Hydration succeeds, the page returns 200, unit tests using
renderToStaticMarkuppass, andastro buildis clean — so it isn't a duplicated-React or rules-of-hooks problem. The warning is purely a side-effect of the renderer probe.Root cause
packages/integrations/react/src/server.jscheck()(lines correspond todist/server.js26-37 in 5.0.5):TestercallsComponent(...args)directly. WhenComponentruns a hook, React's dev dispatcher firesconsole.error("Invalid hook call …")because the call did not originate fromreact-dom/server's normal render path (the dispatcher binding belongs toTester, notComponent). The thrown rules-of-hooks error itself is swallowed by thetry/catch, but theconsole.erroris not. The vnode is still returned,$$typeofstill matches,isReactComponentbecomestrue, and hydration proceeds — the warning is cosmetic, but every Astro user with a hook-using island sees it on every dev request.Reproduction
Minimal: any
client:loadReact component that callsuseStateat the top level, served viaastro devwith@astrojs/react5.x and React 19.What I tried (none worked)
vite.ssr.optimizeDeps.include: ['react','react-dom','react/jsx-runtime','react/jsx-dev-runtime']vite.resolve.dedupeexpandedvite.ssr.resolve.conditions: ['node','import','module','default'](skipreact-server)@astrojs/react5.0.4 → 5.0.5None affected the warning, confirming the source is
check(), not module duplication.Suggested fix
Replace probe-by-invocation with one of:
Component.toString()for transpiled markers (React.createElement(,_jsx(,_jsxs(,jsx(,jsxs(). Cheap, no execution.metadata.componentUrlunconditionally. The existingfilterbranch at line 22 already short-circuits when an explicitopts.include/opts.excludeis set; ametadata.componentUrlending in.jsx/.tsx(or imported via the React renderer's resolved id) is a reliable positive signal without invocation.React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE.ReactCurrentDispatcher.currentto a no-op throwing dispatcher around the invocation so the rules-of-hooks check sees a "valid" render context. Fragile across React minors — not preferred.(1) or (2) are clean and don't require touching React internals.
Local investigation thread
bykplx1/qa-learning-site#223 (comment)