fix: 消除文章页 dev 下的 "Invalid hook call" 告警

现象
dev 下每次打开文章页,终端都会打印 2 次 React 的 "Invalid hook call"。
生产构建没有;首页、/thoughts/、/friends/ 也没有。

根因(用临时 Vite 插件抓的调用栈定位)
不是 React 多副本,也不是 hooks 写法有问题,而是 Astro 的渲染器探测:

1. TableOfContents 首屏 headings 为空 → `return null`(数据在 useEffect 里填,
   SSR 期不执行)
2. @astrojs/react 的 check 会试渲染一次,拿到 null 就认不出这是 React 组件 → false
3. Astro 转向下一个渲染器 @astrojs/mdx
4. MDX 的 check 把组件**当普通函数裸调** `await Component({...})`,不在 React
   渲染树里 → useState 找不到 dispatcher → 报 Invalid hook call → 抛错被 catch
5. 两个渲染器都说不是自己的,Astro 最终仍用 React 正确渲染了组件

所以页面和组件一直是对的,只有日志是错的。生产构建没有是因为 React 生产版
剥掉了这段 dev-only 警告(栈里是 react.development.js)。
Hero 之类的组件不报,是因为它们探测时返回真实 JSX,第 2 步就被 React 认出来了。

修法
去掉 `if (headings.length === 0) return null`,改为始终返回 <nav>,无标题时用
`hidden` 收起,视觉结果不变。这样第 2 步就能认出是 React 组件,根本不会走到
MDX 的探测。代价是文章页 SSR 多出 2 个 display:none 的空 <nav>(两个 TOC 实例各一个),
以及 island 上多了 Astro 的 await-children 标记。

另一条路(未采用):项目里 0 个 .mdx,直接删掉 @astrojs/mdx 也能消除告警,
且零 DOM 变化。选择保留 MDX 是为了不因一个纯噪音的警告砍掉一个能力。

验证
dev 下逐个请求 5 类页面(含之前必现的 2 个文章页)告警数全为 0;
生产构建与基线的差异只剩本次有意的隐藏 nav、资源哈希和 island uid;
pnpm build 41 页 + pagefind 通过;astro check 仍是 1 error/0 warnings(原有报错)。
This commit is contained in:
2026-09-17 18:16:43 +08:00
parent f4a49ef94c
commit 93362376f8
+13 -3
View File
@@ -285,10 +285,20 @@ export function TableOfContents({ showHeader = true }: TableOfContentsProps) {
}, 2500) }, 2500)
} }
if (headings.length === 0) return null // 注意:这里不能 `return null`。
//
// Astro 靠「渲染器探测」决定交给哪个框架渲染:@astrojs/react 的 check 会试渲染一次,
// 拿到 null 就认不出这是 React 组件;接着轮到 @astrojs/mdx 的 check,而它是把组件
// 当普通函数**裸调**(不在 React 渲染树里),useState 找不到 dispatcher,于是每次
// SSR 都往控制台打一条 "Invalid hook call"。页面本身没错,但那行日志很误导人。
//
// 始终返回 <nav>,没有标题时用 hidden 收起来 —— 视觉结果和 return null 一样。
return ( return (
<nav ref={navRef} aria-label="目录" className="toc-nav"> <nav
ref={navRef}
aria-label="目录"
className={cn("toc-nav", headings.length === 0 && "hidden")}
>
{showHeader ? ( {showHeader ? (
<div className="flex items-center gap-2 mb-3"> <div className="flex items-center gap-2 mb-3">
<div className="w-1 h-4 bg-primary rounded-full" /> <div className="w-1 h-4 bg-primary rounded-full" />