在MDX里嵌入交互组件

MDX 让你在 Markdown 正文里直接使用 React 组件。这篇讲清 client:* 指令为什么不能省、在 Astro 里用 shadcn/ui 会踩的三个坑,以及什么时候压根不该用 MDX。

普通的 .md 已经能满足大部分写作需求,但当你想在正文里嵌入真正的交互组件时, .mdx 就派上用场了。

直接引入组件

在文件顶部像写普通 JS 一样 import,然后在正文任意位置使用:

mdx
import { Button } from '@/components/ui/button';

<Button>点我</Button>

下面就是一个真实渲染、可点击的 shadcn/ui 按钮:

client:* 不是可选项

上面那行如果写成 <Button>点我</Button>,页面上照样会出现一个长得一模一样的按钮—— 但它点了没反应

因为 Astro 默认只把 React 组件渲染成静态 HTML,不发送任何 JS。要让它真的能交互, 必须显式加水合指令:

指令何时水合适用
client:load页面加载即刻首屏就要能用的(主题切换、搜索框)
client:visible滚动进视口才加载正文里的示例组件,默认选它
client:idle浏览器空闲时次要交互,不急着用的

文章正文里的演示组件几乎总该用 client:visible——读者可能根本滚不到那里, 没必要为一个也许不会被看到的按钮提前下载 JS。

这也是「能不加就不加」的由来:每个 client:* 都会往页面里塞一份运行时。 纯展示的组件(图标、徽章、卡片外壳)不加指令,就是零 JS 的静态 HTML。

在 Astro 里用 shadcn/ui 的三个坑

这三个都不报错,只是静默地不生效,所以特别费时间。

1. <Button asChild> 会失效

想做「长得像按钮的链接」时,React 里的常规写法是 <Button asChild><a href="…">。 在 Astro 里这样写会得到一段纯文本——Astro 传进去的 children 是已经渲染好的 HTML slot,Radix 的 Slot 没办法穿过 Astro 边界把按钮的类名合并到 <a> 上。

正确做法是直接用 buttonVariants() 取出类名:

astro
<a href="/blog" class={buttonVariants({ variant: 'outline' })}>看文章</a>

顺带的好处是这个链接完全不需要 JS。

2. 组合型组件不能拆开写在 .astro

<Avatar><AvatarFallback> 这种父子配套的 Radix 组件,如果当成两个独立元素写在 .astro 文件里,它们会各自变成独立的孤岛,丢掉 React context,构建时直接报 must be used within <Avatar>

要么包成一个 .tsx 组件整体引入,要么——如果只是装饰——干脆用普通 HTML 加 Tailwind 类写掉。

3. 传样式要用 className,不是 class

给 React 组件传样式时,Astro 不会帮你把 class 转成 className。写成 class 的话,它会被 shadcn 内部的 cn(className) 直接忽略:没有报错、没有警告,页面只是 悄悄少了那段间距。

astro
<Separator className="my-8" />   <!-- 生效 -->
<Separator class="my-8" />       <!-- 样式静默丢失 -->

原生 HTML 元素照常写 class,只有 React 组件要用 className

什么时候别用 MDX

  • 只是想要一个好看的框:用 Markdown 的引用块加 CSS 就够了,别为此拉进一个组件
  • 只是想放张图或一段代码.md 本来就支持
  • 同样的效果原生 HTML 能做<details><dialog> 现在都很能打,不必上 React

MDX 真正的价值场景只有一个:读者需要动手操作才能理解的东西——可交互的图表、 参数可调的演示、实时预览的配置器。其余情况用普通 Markdown,越简单越好。

评论

评论加载中……

登录后再评论

注册要用邮箱收个验证码,只为确认邮箱能收信,不会拿去做别的。账号设置