Notes
← All notes
·frontend·5 min

react-markdown 的 GFM 陷阱:為什麼 h3 和 table 會消失

react-markdown 用錯 export,GFM 元素在 re-render 後靜悄悄消失。記錄診斷過程和三行修正。

#React#Markdown#Debug

在 personal-site 的文章詳細頁上,我用 react-markdown 渲染文章內文。文章裡有 h3 標題、粗體、table,看起來沒問題——直到我加了 Firestore fetch 之後。

頁面載入後,Firestore 回傳資料、更新 state,然後 h3、**粗體**、表格、--- 分隔線全部消失。h2 還在。完全沒有 console error,TypeScript 也沒報錯。

這是我遇到的兩個陷阱,以及它們為什麼同時藏在一起。


第一個坑:沒裝 remark-gfm

react-markdown 預設只處理 CommonMark 規格。表格(pipe syntax | col | col |)、刪除線、task list、autolink 這些屬於 GFM(GitHub Flavored Markdown) 擴充語法,預設不支援。

如果沒裝 remark-gfm,表格不會報錯,只是直接渲染成純文字:

| 名稱 | 說明 |
|------|------|
| LCP  | 最大內容渲染 |

會變成一行奇怪的文字,不是 <table>

修法很直接:

npm install remark-gfm

光這樣還不夠——第二個坑才是真正的問題。


第二個坑:MarkdownHooks + inline array

react-markdown 有兩個 export:

Export行為
Markdown(default export)同步渲染
{ MarkdownHooks }非同步渲染,內部使用 useEffect

我用的是 MarkdownHooks,而且 remarkPlugins 是這樣傳的:

// 問題寫法
<MarkdownHooks remarkPlugins={[remarkGfm]}>{body}</MarkdownHooks>

[remarkGfm] 是 inline array literal。每次 component re-render,這個 array 都是全新的 reference。

MarkdownHooks 內部的 useEffect 依賴這個 array reference 決定要不要重新執行 async processor。新 reference → 每次 re-render 都重跑 → 非同步處理的結果在 timing gap 裡遺失。

結果:Firestore fetch 回來更新 state → component re-render → useEffect 重跑 → GFM 元素在非同步間隙消失。


為什麼 h2 還在

這是最讓人困惑的地方:h2 正常,h3 不見。

MarkdownHooks 在非同步 pass 之前有一個初始同步 pass,部分處理了 ATX heading(# 語法)。h2 剛好在這個初始 pass 裡被正確渲染,所以看起來沒問題。

h3 在 GFM pass 才被完整處理,timing gap 發生後就不見了。

這讓問題看起來像「只有 h3 有 bug」,而不是「整個 async processor 在重跑」。


正確用法

三行修正:

import Markdown from "react-markdown";   // sync default export,不是 MarkdownHooks
import remarkGfm from "remark-gfm";

<Markdown remarkPlugins={[remarkGfm]}>{body}</Markdown>

同步 Markdown 不依賴 useEffect,inline array 不會造成問題。


什麼時候才用 MarkdownHooks

MarkdownHooks 的存在是有理由的:支援需要非同步處理的 remark/rehype plugin(例如 syntax highlighting 需要非同步 fetch 語言包)。

如果確實需要它,remarkPlugins 要用 useMemo 穩定 reference:

const plugins = useMemo(() => [remarkGfm], []);
<MarkdownHooks remarkPlugins={plugins}>{body}</MarkdownHooks>

少了 useMemo,每次 re-render 都會觸發 useEffect,回到同樣問題。


驗收方式

修完之後,用 Playwright 確認 GFM 元素有出現:

expect(await page.locator('.prose h3').count()).toBeGreaterThan(0);
expect(await page.locator('.prose strong').count()).toBeGreaterThan(0);
expect(await page.locator('table').count()).toBeGreaterThan(0);

這三個 locator 同時通過,代表 GFM 渲染和 re-render 行為都正確。


總結

問題原因修正
表格渲染成純文字沒裝 remark-gfmnpm install remark-gfm + remarkPlugins={[remarkGfm]}
re-render 後 GFM 元素消失MarkdownHooks + inline array 每次產生新 reference換成 sync Markdown default export
只有 h3 不見,h2 正常初始同步 pass 部分處理 h2,讓問題看起來像 plugin 問題同上,根本原因是 MarkdownHooks timing 問題