# React (/zh/customize/react-components)

<!-- agent-signals: reading_time_min: 2 · est_tokens: 1309 · updated: 2026-07-30 -->
Related: [自定义域名](/zh/customize/custom-domain.md), [主题](/zh/customize/themes.md), [字体](/zh/customize/fonts.md), [自定义脚本](/zh/customize/custom-scripts.md), [自定义 404 页面](/zh/customize/custom-404-page.md)

直接在 MDX 文件中使用 [React 组件](https://react.dev) 和 [hooks](https://react.dev/reference/react/hooks) 在文档中构建交互式元素。

<div id="inline-components">
  ## 内联组件 [#内联组件]
</div>

直接在你的 MDX 文件中声明组件：

<Counter />

```mdx
export const Counter = () => {
  const [count, setCount] = useState(0)
  const increment = () => setCount(count + 1)
  const decrement = () => setCount(count - 1)

  return (
    <div>
      <button onClick={decrement}>-</button>
      <span>{count}</span>
      <button onClick={increment}>+</button>
    </div>
  )
}

<Counter />
```

<div id="import-components">
  ## 导入组件 [#导入组件]
</div>

组件文件必须位于 `/snippets/` 文件夹中。更多信息请参阅 [可复用片段](/zh/create/reusable-snippets)。

<Note>
  不支持嵌套导入。请将所有被引用的组件直接导入到父 MDX 文件中。
</Note>

在 `snippets/` 中创建一个组件文件：

```mdx title="/snippets/color-generator.jsx [expandable]"
export const ColorGenerator = () => {
  const [hue, setHue] = useState(180)
  const [saturation, setSaturation] = useState(50)
  const [lightness, setLightness] = useState(50)
  const [colors, setColors] = useState([])

  useEffect(() => {
    const newColors = []
    for (let i = 0; i < 5; i++) {
      const l = Math.max(10, Math.min(90, lightness - 20 + i * 10))
      newColors.push(`hsl(${hue}, ${saturation}%, ${l}%)`)
    }
    setColors(newColors)
  }, [hue, saturation, lightness])

  const copyToClipboard = (color) => {
    navigator.clipboard
      .writeText(color)
      .then(() => {
        console.log(`Copied ${color} to clipboard!`)
      })
      .catch((err) => {
        console.error("Failed to copy: ", err)
      })
  }

  return (
    <div className="p-4 border dark:border-zinc-950/80 rounded-xl not-prose">
      <div className="space-y-4">
        <div className="space-y-2">
          <label className="block text-sm text-zinc-950/70 dark:text-white/70">
            Hue: {hue}°
            <input
              type="range"
              min="0"
              max="360"
              value={hue}
              onChange={(e) => setHue(Number.parseInt(e.target.value))}
              className="w-full h-2 bg-zinc-950/20 rounded-lg appearance-none cursor-pointer dark:bg-white/20 mt-1"
              style={{
                background: `linear-gradient(to right, 
                  hsl(0, ${saturation}%, ${lightness}%), 
                  hsl(60, ${saturation}%, ${lightness}%), 
                  hsl(120, ${saturation}%, ${lightness}%), 
                  hsl(180, ${saturation}%, ${lightness}%), 
                  hsl(240, ${saturation}%, ${lightness}%), 
                  hsl(300, ${saturation}%, ${lightness}%), 
                  hsl(360, ${saturation}%, ${lightness}%))`,
              }}
            />
          </label>

          <label className="block text-sm text-zinc-950/70 dark:text-white/70">
            Saturation: {saturation}%
            <input
              type="range"
              min="0"
              max="100"
              value={saturation}
              onChange={(e) => setSaturation(Number.parseInt(e.target.value))}
              className="w-full h-2 bg-zinc-950/20 rounded-lg appearance-none cursor-pointer dark:bg-white/20 mt-1"
              style={{
                background: `linear-gradient(to right, 
                  hsl(${hue}, 0%, ${lightness}%), 
                  hsl(${hue}, 50%, ${lightness}%), 
                  hsl(${hue}, 100%, ${lightness}%))`,
              }}
            />
          </label>

          <label className="block text-sm text-zinc-950/70 dark:text-white/70">
            Lightness: {lightness}%
            <input
              type="range"
              min="0"
              max="100"
              value={lightness}
              onChange={(e) => setLightness(Number.parseInt(e.target.value))}
              className="w-full h-2 bg-zinc-950/20 rounded-lg appearance-none cursor-pointer dark:bg-white/20 mt-1"
              style={{
                background: `linear-gradient(to right, 
                  hsl(${hue}, ${saturation}%, 0%), 
                  hsl(${hue}, ${saturation}%, 50%), 
                  hsl(${hue}, ${saturation}%, 100%))`,
              }}
            />
          </label>
        </div>

        <div className="flex space-x-1">
          {colors.map((color, idx) => (
            <div
              key={idx}
              className="h-16 rounded flex-1 cursor-pointer transition-transform hover:scale-105"
              style={{ backgroundColor: color }}
              title={`Click to copy: ${color}`}
              onClick={() => copyToClipboard(color)}
            />
          ))}
        </div>

        <div className="text-sm font-mono text-zinc-950/70 dark:text-white/70">
          <p>
            Base color: hsl({hue}, {saturation}%, {lightness}%)
          </p>
        </div>
      </div>
    </div>
  )
}
```

然后导入并使用它：

```mdx
import { ColorGenerator } from "/snippets/color-generator.jsx"

<ColorGenerator />
```

<div id="considerations">
  ## 注意事项 [#注意事项]
</div>

* **SEO**：搜索引擎可能无法完全索引由客户端渲染的动态内容。
* **首次加载**：访客在组件渲染之前可能会看到闪烁。
* **可访问性**：确保屏幕阅读器能够播报动态内容的变化。
* **优化依赖数组**：仅在 `useEffect` 中包含必要的依赖。
* **缓存昂贵操作**：在适当之处使用 `useMemo` 或 `useCallback`。
* **减少重新渲染**：将大型组件拆分为较小的组件。
* **懒加载**：组件代码无法懒加载。所有组件都会随页面一起加载。如果某个组件在挂载时会执行资源密集型操作（例如获取数据或进行大量计算），请使用条件渲染，将其挂载推迟到用户交互时。
