Using React-Markdown to Quickly Migrate Markdown Documents to React Components

A recent project I worked on involved documentation that needed to be displayed within our React-based web application. The documentation was Markdown-based but needed to appear in the same style as the rest of the application’s components. My first reaction was that this would be an extremely tedious task. The task of converting every header, code block, and list into the correctly-formatted component was threatening to amount to an unappealing number of hours. If you are facing a similarly overwhelming situation, there is good news! React-markdown is a package that allows you to easily convert Markdown to React components with flexible customization.

Define the Conversions

The first step, aside from installing the package, of course, was to set up a component mapping file, component-mapping.tsx. All the specifications for how the various parts of Markdown’s syntax should appear could be defined and exported from here.

Here is an example of a slightly more detailed conversion for all header values and a conversion for a hyperlink that simply tacks on a new set of styles.

import { Typography, useTheme } from '@material-ui/core';
import { useStyles } from './styles';
import _ from 'lodash';

// return the corresponding MaterialUI Typography component for each header value
const MakeTypographyComponent = (
    props: any,
    header: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6',
): JSX.Element => {
    const theme = useTheme();
    const classes = useStyles(theme);

    return (
        <Typography
            variant={header}
            data-testid={`${_.kebabCase(_.lowerCase(props.children))}-header`}
            {...props}
            className={header !== 'h1' && classes.subheader}
        />
    );
};

const TypographyWithContentAnchor =
    (headerSize: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6') =>
    ({ ...props }): JSX.Element =>
        MakeTypographyComponent(props, headerSize);

const AHref = ({ ...props }): JSX.Element => {
    const theme = useTheme();
    const classes = useStyles(theme);

    return <a role="link" {...props} className={classes.href} /> ;
};

export const getComponentsForPage = (): any => ({
    h1: TypographyWithContentAnchor('h1'),
    h2: TypographyWithContentAnchor('h2'),
    h3: TypographyWithContentAnchor('h3'),
    h4: TypographyWithContentAnchor('h4'),
    h5: TypographyWithContentAnchor('h5'),
    h6: TypographyWithContentAnchor('h6'),
    a: AHref,
});

These conversions can be as simple or as complicated as you want to make them. If you like watching the world burn, you could even map them all to completely incorrect components!

Note that react-markdown will pass along all the Markdown content through the props. The actual text value is assigned to props.children.

Pass in the Source

A ReactMarkdown component accepts the source content in its children prop. Many of the examples show how Markdown content can be directly passed in:

import React from 'react'
import ReactMarkdown from 'react-markdown'
import ReactDom from 'react-dom'

ReactDom.render(# Hello, *world*!, document.body)

Once you are working with multiple large pages of Markdown content, this is a less feasible option. It is also a decent idea to add some checks for invalid sources or account for loading issues. The following example component uses fetch within a useEffect hook to load the source Markdown file. If it fails to load, or is being passed an incorrect source file, an error component will display in its place.


import ReactMarkdown from 'react-markdown';
import { useEffect, useState } from 'react';
import { getComponentsForPage } from 'src/client/markdown-content/markdown-to-component/component-mapping';
import { useStyles } from './styles';
import { DisplayError } from '../DisplayError';

type MarkdownProps = {
    source: string;
};

export const MarkdownContent = (props: MarkdownProps): JSX.Element => {
    const classes = useStyles();

    const [postMarkdown, setPostMarkdown] = useState('');

    useEffect(() => {
        try {
	          // source file must be a .md file
            if (!new RegExp(/^.*.(md)$/).test(props.source)) {
                throw new Error();
            }
	          // fetch the contents of the Markdown source file
            void fetch(props.source)
                .then((response) => response.text())
                .then((text) => {
                    setPostMarkdown(text);
                });
        } catch (e) {
	          // something went wrong -- display custom error component instead
            setPostMarkdown('error');
        }
    }, [props.source]);

    return postMarkdown === 'error' ? (
        <div className={classes.errorDisplay}>
            <DisplayError errorMessage={`Failed to load markdown for ${props.source}`} />
        </div>
    ) : (
        <div className={classes.contentBlock}>
            <ReactMarkdown
                children={postMarkdown}
                components={getComponentsForPage()}
            />
        </div>
    );
};

The ReactMarkdown component defined here grabs the earlier defined component conversions using the getComponentsForPage call. The components prop can also be assigned with more specific inline customization, as in this example. Or, you can use Typescript object mapping in the component-mapping file to create specific combinations of syntax conversions

Now, any other component with a defined Markdown source file can use this MarkdownContent component:

import source from 'src/client/markdown-content/markdown-pages/ExampleContent.md';
import { MarkdownContent } from '../MarkdownContent';

export const ExampleContent = (): JSX.Element => <MarkdownContent source={source} /> ;

The Final Result!

With our handy component-mapping setup and some simple style additions, this:


# Here is an example page!

[Here is a link](https://www.example.com/)

---

```
Some code here
```

##### Very Informative Stuff

turns into this (no tedious repetitive converting required):

I thought react-markdown was a comprehensive and flexible tool. It made uniform customization of many documentation files efficient and simple.

** Note that some syntax items like tables, checkboxes, and strikethroughs require that you add specific plugins to the ReactMarkdown component.