Skip to content

Migrate from react-markdown

I'd read this before you switch. It covers what's different as well as what matches.

Why we don't call it a drop-in replacement#

The props match and all 39 compared cases produce identical markup, but three things differ. Two of them can fail silently, so they're listed first.

Three differences#

className is ignored rather than rejected#

react-markdown 10 throws Unexpected className prop, remove it. This renderer doesn't have a className prop either, but it ignores it at runtime instead of throwing. TypeScript rejects it, so in TypeScript you'll get an error. JavaScript code that still passes className will silently lose the wrapper.

Wrap the component in your own element instead.

-<Markdown className="prose">{content}</Markdown>
+<div className="prose">
+  <Markdown>{content}</Markdown>
+</div>

MarkdownAsync and MarkdownHooks do not exist#

This renderer is synchronous only. If you use either export, or a remark or rehype plugin that does asynchronous work, keep react-markdown for those call sites. The codemod leaves them alone and tells you about them.

remarkPlugins on a precompiled document#

A plugin that changes the dialect, such as remark-gfm, can't apply to text that has already been parsed. Pass it when compiling, or use an extension on the preset.

-<Markdown remarkPlugins={[remarkGfm]} document={doc} />
+const doc = compileMarkdown(src, { preset: defineMarkdownPreset({ extensions: [gfm()] }) })
+<Markdown document={doc} />

Plugins that transform the tree still run on a precompiled document.

What matches#

Eleven props produce identical markup against a pinned react-markdown@10.1.0: children, components, remarkPlugins, rehypePlugins, remarkRehypeOptions, allowedElements, disallowedElements, allowElement, skipHtml, unwrapDisallowed and urlTransform. The default URL policy is the same algorithm and agrees on every probe URL.

The matrix is generated from the test run, so it stays in sync with what the tests actually check.

The full matrix

The codemod#

It does a dry run by default, and it only changes imports.

npx rmk-migrate 'src/**/*.tsx'          # report only
npx rmk-migrate 'src/**/*.tsx' --write  # apply

It keeps your alias and quote style.

-import ReactMarkdown from "react-markdown"
+import ReactMarkdown from "@react-markdown-kit/renderer"

When it isn't sure, it leaves the code alone. MarkdownAsync, MarkdownHooks and imports that reach into react-markdown/lib/* are left untouched with an explanation. It also flags rehype-raw used without rehype-sanitize instead of quietly carrying that setup across, because that combination executes author HTML.

Check against your own content first#

Render your real content through both implementations and compare the output. This runs locally and nothing leaves your machine.

npx rmk-compare 'content/**/*.md' --gfm
3 document(s) compared
  matching   3 (100%)
  differing  0

It exits non-zero on a difference, so you can gate a pull request on it.

Suggested order#

  1. Run rmk-compare on your content and read the differences.
  2. Run rmk-migrate as a dry run and read what it won't touch.
  3. Fix className call sites by hand.
  4. Apply with --write, then run your own tests.

Next#

Compatibility matrix · Renderer overview · Security model

Last updated on