How to Safely Render Markdown From a React Component
Tags: react, markdown, tutorial
Published: 2021-07-30 04:46:28

_by author [Fortune Ikechi](https://blog.openreplay.com/authors/fortune-ikechi)_ 

[React](https://github.com/remarkjs/react-markdown)[-](https://github.com/remarkjs/react-markdown)[markdown](https://github.com/remarkjs/react-markdown) is a library that provides the React component to render the Markdown markup. Built on [remark](https://github.com/remarkjs/remark), a markdown preprocessor. 

With react-markdown, you can safely render markdown and not have to rely on the [`dangerouslySetInnerHTML`](https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml) prop, instead React markdown uses a syntax tree to build a virtual DOM for your markdown. 

Markdown is a lightweight markup language that is easy to learn and work with it, created to include basic tags, it helps to create editorial content easily. However, its important to note that React markdown does not support HTML by default and therefore prevents script injection, making it safer to use. 

Script injection is a security vunerability that allows malicious code to the user interface element of an application, you can read more about it [here.](https://www.itprotoday.com/development-techniques-and-management/script-injection-attacks)


## Why use React markdown

There are many markdown component libraries available for use in React applications, some of which include [react-markdown-editor-lite](https://openbase.com/js/react-markdown-editor-lite), [commonmark-react-renderer](https://openbase.com/js/commonmark-react-renderer) and [react-remarkable](https://openbase.com/js/react-remarkable). These libraries although great all focus on `dangerouslySetInnerHTML`, however react-markdown uses a syntax tree to build the virtual DOM that allows for updating only the changing DOM instead of completely overwriting it. React markdown also supports [CommonMark](https://www.smashingmagazine.com/2020/12/commonmark-formal-specification-markdown/) and has extensions to support custom syntax and a list of plugins to extend it’s features. 

**React markdown vs remark**
`react-markdown` is a library that provides the React component to render the Markdown markup while [remark](https://github.com/remarkjs/remark) is a markdown preprocessor built on [micromark](https://github.com/micromark/micromark). It inspects, parses and transforms markdowns. 

## Getting Started with React markdown

In this section, we will build a markdown blog using react markdown, with this application, users can write an article in markdown which when completed can be previewed in plain text. First, let’s create a new project and install react-markdown 

## Initializing Project

To initialize a new react typescript project, run the command below


```
npx create-react-app react-markdown-blog --template typescript
```


In the code above, we initialized a new react typescript project with the application name “react-markdown-app”, this can be replaced with whatever name you choose. 

Next, let’s install dependencies and start our application’s development server below 


```
yarn add autoprefixer postcss-cli postcss tailwindcss moment react-markdown
```


In the code above, we installed the following dependencies, `postcss-cl``i`, `postcss` for transforming styles with JS plugins and help us lint our CSS and `tailwindcss` for our styling, `react-markdown` for rendering our markdown component, and `moment` for parsing our dates, `autoprefixer` for adding vendor prefixes to our CSS rules. 

Next, we need to setup tailwindcss for our project, type the following command in your terminal to generate a tailwind config file in your project directory.


```
npx tailwind init tailwind.config.js
```

By default tailwindcss prevents markdown styles from displaying. To fix that, install a tailwind plugin `@tailwindcss/typography` according to [documentation](https://v1.tailwindcss.com/docs/typography-plugin),


> The [@tailwindcss/typography plugin](https://github.com/tailwindcss/typography) adds a set of customizable `prose` classes that you can use to add beautiful typographic defaults to any vanilla HTML, like the output you'd get after parsing some Markdown, or content you pull from a CMS.

Inside the `@tailwindcss.config.js` file, add the code below


```javascript
module.exports = {
  purge: [],
  darkMode: false, // or 'media' or 'class'
  theme: {
    extend: {
      typography: {
        DEFAULT: {
          css: {
            color: "#FFF",
            a: {
              color: "#4798C5",
              "&:hover": {
                color: "#2c5282",
              },
            },
            h1: {
              color: "#FFF",
            },
            h2: {
              color: "#FFF",
            },
            h3: {
              color: "#FFF",
            },
            h4: { color: "#FFF" },
            em: { color: "#FFF" },
            strong: { color: "#FFF" },
            blockquote: { color: "#FFF" },
            code: { backgroundColor: "#1A1E22", color: "#FFF" },
          },
        },
      },
    },
  },
  variants: {
    extend: {},
  },
  plugins: [require("@tailwindcss/typography")],
};
```
    

Run the below command to generate a postcss config file in your project directory


```
touch postcss.config.js
```

Add the code below in the file;


```javascript
const tailwindcss = require('tailwindcss');
module.exports = {
    plugins: [
        tailwindcss('./tailwind.config.js'),
        require('autoprefixer')
    ],
};
```
    

Create a folder called `styles` in src and add 2 files `main.css` (where generated tailwind styles will enter) and `tailwind.css` for tailwind imports.

Inside `tailwind.css`,  add the codes below


```javascript
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
```


Update your scripts in `package.json` to build tailwind styles when the dev server starts


```javascript
"scripts": {
  "start": "npm run watch:css && react-scripts start",
  "build": "npm run watch:css && react-scripts build",
  "test": "react-scripts test",
  "eject": "react-scripts eject",
  "watch:css": "postcss src/styles/tailwind.css -o src/styles/main.css"
}
```


Lastly, add `main.css`  to `src/index.ts`, the file should like this;


```javascript
import React from "react";
import ReactDOM from "react-dom";
import "./styles/main.css";
import "./index.css";
import App from "./App";

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById("root")
);
```


To start our development server, let’s start our development server using the command below 


```
yarn start
```

OR using NPM


```
npm start 
```



## Building Navbar Component

Here, we will create components for our applications, first navigate to your `src` folder and inside it, create a new folder named `components`, this will house all of our project components. Inside your `components` folder, create a new directory called `Navbar`, inside this directory, create a new file called `Navbar.tsx`. 

Firstly, add `react-router-dom` for navigation within the app and two react-markdown plugins to extends react-markdown features; we’ll be passing them to the Markdown component.


```
yarn add react-router-dom remark-gfm rehype-raw
```

Inside this file, let’s create our `Navbar` with the code block below:


```javascript
import { Link, NavLink } from "react-router-dom";
const Navbar = () => {
    return (
        <nav className="mt-6 w-10/12 mx-auto flex justify-between items-center">
            <Link
                style={{ fontStyle: "oblique" }}
                className="text-xl tracking-widest logo"
                to="/"
            >
                Markdown Blog
            </Link>
            <div className="flex-items-center">
                <NavLink
                    activeClassName="border-b-2"
                    className="mr-6 tracking-wider"
                    to="/write-article"
                    exact
                >
                    Write An Article
                </NavLink>
                <NavLink
                    activeClassName="border-b-2"
                    className="tracking-wider"
                    to="/profile"
                    exact
                >
                    Profile
                </NavLink>
            </div>
        </nav>
    );
};
export default Navbar;
```


In the code blog above, we created a function component called `Navbar`, inside it we created a `Navbar` component, giving our application a title, we also added links to a `Profile` page and to a page where user can write an article. 

Next, we will create a blog card that will feature our blog posts, this will show each blog post written and we’d use `react-markdown` to render the articles in plain text with styles. Let’s do that in the section below.



## Adding Helper functions

Before we build our BlogCard, we need to add some helperFunctions that will help us:

1. Delete, edit, save and fetch posts from localStorage (our DB).
2. Format the date of the post.
3. Truncate the post body so our BlogCard doesn’t get too big when the body is very lengthy.

Firstly, inside the `src` folder create a folder called `utils`, in there create a new file and name it `helperFunctions.ts` add the code block below.


```javascript
import moment from "moment";

export const truncateText = (text: string, maxNum: number) => {
  const textLength = text.length;
  return textLength > maxNum ? `${text.slice(0, maxNum)}...` : text;
};

export const formatDate = (date: Date) => {
  return moment(date).format("Do MMM YYYY, h:mm:ss a");
};
```
     

Above, we have a function called `textTruncate` that accepts in two arguments; text and maxNum.
it checks if the text has a length greater than the maxNumber we want to display, if yes we remove the surplus and add ‘…’ else we return the text.

 The second function; `formatDate` basically formats a date object passed to it to the format we want to display in our app. Let’s add more functions to manipulate our posts with below.

Create another file inside `utils` and name it `server.ts` it will contain the functions to add, delete, fetch and edit a post. Add the code below


```javascript
import { IBlogPost } from "../components/BlogCard/BlogCard";

export const savePost = (post: Partial<IBlogPost>) => {
  if (!localStorage.getItem("markdown-blog")) {
    localStorage.setItem("markdown-blog", JSON.stringify([post]));
  } else {
    const posts = JSON.parse(localStorage.getItem("markdown-blog") as string);
    localStorage.setItem("markdown-blog", JSON.stringify([post, ...posts]));
  }
};

export const editPost = (newPostContent: IBlogPost) => {
  const posts: IBlogPost[] = JSON.parse(
    localStorage.getItem("markdown-blog") as string
  );
  const postIdx = posts.findIndex((post) => post.id === newPostContent.id);
  posts.splice(postIdx, 1, newPostContent);
  localStorage.setItem("markdown-blog", JSON.stringify(posts));
};
```


In the code we added above, `savePost` functions takes in an object of type BlogPost which we will create in our BlogCard component. It checks if we have saved any post to our browser’s localStorage from this app with the key  *markdown-blog**,* if **none is found we add the post to an array and save it to localStorage. Otherwise we fetch the posts we already have and include it before saving.

We have also added a function called `editPost` which we will use to edit posts, it takes in the `newContent` object which will contain updated post properties, in here we fetch all posts,  find the index of the post we want to edit and splice it out and replace it with the `newContent`  at that index in the array as seen in line 34. after splicing we save it back to locatStorage. 

Let’s add the other two functions below.


```javascript
export const getPosts = () => {
  if (!localStorage.getItem("markdown-blog")) {
    return [];
  } else {
    const posts = JSON.parse(localStorage.getItem("markdown-blog") as string);
    return posts;
  }
};

export const deletePost = (id: string) => {
  const posts: IBlogPost[] = JSON.parse(
    localStorage.getItem("markdown-blog") as string
  );
  const newPostList = posts.filter((post) => post.id !== id);
  localStorage.setItem("markdown-blog", JSON.stringify(newPostList));
};
```


`getPost` functions fetches our posts from localStorage and returns them if we have that post in localStorage or an empty array if we don’t.

 `deletePost` takes in an id as an argument, fetches the all posts from loalStorage, filters out the one with the id passed to this function and saves the rest to localStorage.


## Building Blog Card component

As we have now added our helper functions, let’s create a blog card component for our application, to do this we’d first create a typescript interface (which we will pass to our savePost and editPost functions created above) to define the `type`  of each prop to be passed to the `BlogCard` component, next we will create our `BlogCard` component, let’s do that below 


```javascript
import { Link } from "react-router-dom";
import ReactMarkdown from "react-markdown";
import gfm from "remark-gfm";
import rehypeRaw from "rehype-raw";
import { formatDate, truncateText } from "../../utils/helperFunctions";
import { deletePost } from "../../utils/server";

export interface IBlogPost {
    id: string;
    title: string;
    url: string;
    date: Date;
    body: string;
    refresh?: () => void;
}
const BlogCard: React.FC<IBlogPost> = ({
    id,
    title,
    body,
    url,
    date,
    refresh,
}) => {
    const formattedDate = formatDate(date);
    const content = truncateText(body, 250);
    const handleDelete = () => {
        const yes = window.confirm("Are you sure you want to delete this article?");
        yes && deletePost(id);
        refresh && refresh();
    };
```


In the code above, we created an interface for our `BlogCard` component, imported React-Markdown and the plugins we installed earlier and also helper functions to delete the post, format the post date and truncate the post text.


- `gfm` is a [remark plugin](https://github.com/remarkjs/remark-gfm) that adds support for strikethrough, table, tasklist and URLs 
- `rehypeRaw` makes react-markdown parse html incase we pass html elements inbetween markdown text. This is dangerous and usually not adviceable as it defeats the purpose of react-markdown not rerendering html to prevent html injection but for the purpose of of learning we will use it.

Next, let’s complete our component with the code below:


```jsx
return (
    <section
        style={{ borderColor: "#bbb" }}
        className="border rounded-md p-4 relative"
    >
        <div className="controls flex absolute right-4 top-3">
            <Link
                title="Edit this article"
                className="block mr-5"
                to={`/edit-article/${id}`}
            >
                <i className="far fa-edit" />
            </Link>
            <span
                role="button"
                onClick={handleDelete}
                title="Delete this article"
                className="block"
            >
                <i className="fas fa-trash hover:text-red-700" />
            </span>
        </div>
        <h3 className="text-3xl font-bold mb-3">{title}</h3>
        <div className="opacity-80">
            <ReactMarkdown
                remarkPlugins={[gfm]}
                rehypePlugins={[rehypeRaw]}
                className="prose"
                children={content}
            />
            &nbsp;
            <Link
                className="text-blue-500 text-sm underline hover:opacity-80"
                to={`${url}`}
            >
                Read more
            </Link>
        </div>
        <p className="mt-4">{formattedDate}</p>
    </section>
);
};
export default BlogCard;
```


Note that the blogpost body we want to render with `react-markdown` is passed to the `children` prop and the other plugins added. The className `prose` is from tailwind, we get it from the [tailwindcss/typography](https://github.com/tailwindcss/typography) plugin we installed and added to our tailwind config to provide support for markdown styles.

We will add a [fontawesome](https://fontawesome.com) CDN link so we can use fontawesome icons for delete and edit as we have in lines 12 and 13 of the bove code block. 

Navigate to your `index.html` file in `public` folder and replace the file’s content with the code block below.


```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="Markdown Blog"
      content="A markdown blog built with React, TS and react-markdown"
    />
    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <!-- Fontawesome CDN link here -->
    <link
      rel="stylesheet"
      href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"
      integrity="sha512-iBBXm8fW90+nuLcSKlbmrPcLa0OT92xO1BIsZ+ywDWZCvqsWgccV3gFoRBv0z+8dLJgyAHIhR35VZc2oM/gI1w=="
      crossorigin="anonymous"
      referrerpolicy="no-referrer" />
      <!-- End of Fontawesome CDN link here -->
    <title>Markdown blog</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
  </body>
</html>
```


This Card will preview each post, give a link to the user to view all contents of the post and from here a user can delete a post or navigate to edit-post page. Now that we’ve created this component, we will go ahead to create pages for our application using these components. 


## Building Home Page

The `Home` page will contain all the posts in the app, we will also add a ‘no data’ state for when there are no posts to show. To do this, first create a new folder called `pages` in our project `src` file, and inside it create another directory called `home`, in here create a new file called `index.tsx`. 


```javascript
// src/pages/home/index.tsx

import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import BlogCard, { IBlogPost } from "../../components/BlogCard/BlogCard";
import { getPosts } from "../../utils/server";

const Homepage = () => {
    const [articles, setArticles] = useState<IBlogPost[]>([]);
    const [refresh, setRefresh] = useState(false);

    useEffect(() => {
        const posts = getPosts();
        setArticles(posts);
    }, [refresh]);
```


In the code above, we imported the `useEffect` and `useState` from `react`, next we imported `Link` from `react-router` this will help us navigate to `write an article` page. We also imported our `BlogCard` component alongside `IB``logPost` interface, to render out each post from the array of posts. Finally, we imported the `getPosts` object from our `utils` directory.

Inside the `useEffect` hook, we are fetching all posts we have saved to localStorage and adding the array of posts to our component state (`useState`), we also created a state which will enable us to make the useEffect refetch posts whenever a post is deleted. 

Next, let’s render our `Home` page using the code block below:


```jsx
return (
        <div className="mt-8 mb-20 w-3/5 mx-auto">
            <h1 className="mb-6 text-xl">Welcome back, Fortune</h1>
            <section className="articles mt-4">
                {articles?.length ? (
                    articles.map((article) => (
                        <article key={article?.id} className="my-4">
                            <BlogCard
                                title={article?.title}
                                id={article?.id}
                                url={`/article/${article?.id}`}
                                body={article?.body}
                                date={article?.date}
                                refresh={() => setRefresh(!refresh)}
                            />
                        </article>
                    ))
                ) : (
                    <div className="mt-20 flex flex-col items-center justify-center">
                        <h2 className="text-2xl">No article right now.</h2>
                        <Link className="block text-blue-500 underline text-sm mt-6" to="/write-article">Add article</Link>
                    </div>
                )}
            </section>
        </div>
    );
};

export default Homepage;
```


In the code block above, we rendered our `Home` page with styles using tailwind classes, we mapped through the array of posts and rendered them with our `BlogCard` component and also added a no data state incase there are posts to render.


## Building Post Page

In this section, we will create a page for a user to post a new blog post. Inside your `pages` directory, create a new folder called post and inside it create a new file named `index.tsx`, add the code block below:


```javascript
import { useParams } from "react-router-dom";
import ReactMarkdown from "react-markdown";
import gfm from "remark-gfm";
import rehypeRaw from "rehype-raw";
import { formatDate } from "../../utils/helperFunctions";
import { getPosts } from "../../utils/server";
import { IBlogPost } from "../../components/BlogCard/BlogCard";
```


We have now imported different components we’d need for building our page. Next, we will be getting an `id` param from our url with `useParams`, rendering our post body with ReactMarkdown. let’s do that in the code block below

```javascript
    const Blog = () => {
        const { id } = useParams<{ id: string }>();
    
        const post = getPosts().find((post: IBlogPost) => post.id === id);
```

In the code block above, we created a `Blog` component and added retrieving the post id from the url to get the post from all posts (line 4). Next, we will render our post content below 


```jsx
return (
        <div className="w-4/5 mx-auto mt-16 mb-24">
            {post ? (
                <>
                    <header
                        style={{ background: "#1C313A" }}
                        className="rounded-md mb-10 max-w-9/12 py-12 px-20"
                    >
                        <h1 className="text-2xl text-center font-semibold uppercase">
                            {post?.title}
                        </h1>
                        <p className="mt-4 text-sm text-center">{formatDate(post?.date as Date)}</p>
                    </header>

                    <ReactMarkdown
                      className="prose"
                      remarkPlugins={[gfm]}
                      rehypePlugins={[rehypeRaw]}
                      children={post?.body as string}
                    />
                </>
            ) : (
                <h3>Post not found!</h3>
            )}
        </div>
    );
};
export default Blog;
```


In the code above, we are checking if a post with the id passed to the url is found, if found we show the post title, date and render the text with ReactMarkdown. if the post is not found we render ‘post not found’. If done correctly, your application should look like the image below 


![rendered text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f9n5eefzmjgen8gmhlqx.png)


## Open Source Session Replay
Debugging a web application in production may be challenging and time-consuming. [OpenReplay](https://github.com/openreplay/openreplay) is an Open-source alternative to FullStory, LogRocket and Hotjar. It allows you to monitor and replay everything your users do and shows how your app behaves for every issue. 
It’s like having your browser’s inspector open while looking over your user’s shoulder.
OpenReplay is the only open-source alternative currently available.

![OpenReplay](https://raw.githubusercontent.com/openreplay/openreplay/main/static/replayer.png)

Happy debugging, for modern frontend teams - [Start monitoring your web app for free](https://github.com/openreplay/openreplay).


## Adding Write-A-Post Page

In this section, we will create a page for writing and editing a post, To do this, create a new folder `write` inside your `pages` folder and inside it create a `index.tsx` file and add the code snippet below 


```javascript
import { useEffect } from "react";
import { useState } from "react";
import { useHistory, useParams } from "react-router";
import { editPost, getPosts, savePost } from "../../utils/server";
import { IBlogPost } from "../../components/BlogCard/BlogCard";

const WriteAnArticle = () => {
    const { id } = useParams<{ id: string }>();
    const history = useHistory();
    const [title, setTitle] = useState("");
    const [body, setBody] = useState("");

    useEffect(() => {
      if (id) {
          const post = getPosts().find((post: IBlogPost) => post.id === id);
          post && setTitle(post.title);
          post && setBody(post.body);
      }
    }, [id]);
```


This component handles both editing and adding posts, so in case of edit we use the id of the post which we will get with `useParams` from the url of the page. In the code block above, we initializing a history variable from `useHistory` with which we will navigate the user to the home page after editing or adding new posts. We also have state to hold our post title and body from inputs. 

In the useEffect, if there’s an id in the URL we’re getting the post to be editted from the array of posts and setting it to our component states so that the values show up in our inputs. 

Next, we will create a `submitHandler` function to enable the user to submit a post or save the edits performed.


```javascript
const submitHandler = (e: { preventDefault: () => void }) => {
        e.preventDefault();

        const post = getPosts().find((post: IBlogPost) => post.id === id);
        if (!id) {
            const post = {
                title,
                body,
                date: new Date(),
                id: new Date().getTime().toString(),
            };
            savePost(post);
        } else if (id && post) {
            const updatedPost = {
                ...post,
                title,
                body,
            };
           editPost(updatedPost);
        }
        history.push("/");
    };
```


In the code above, we are handling form submit, if the id deosn’t exist we save a new post with `savePost` helper function, using the title and body from the form and the id and date as the current timestamp of that moment. else we update the post title, body.

We will render our component body below;


```jsx
return (
        <div className="w-3/5 mx-auto mt-12 mb-28">
            <h3 className="text-3xl text-center capitalize mb-10 tracking-widest">
                Write a post for your blog from here
            </h3>
            <form onSubmit={submitHandler} className="w-10/12 mx-auto">
                <input
                    className="w-full px-4 mb-6 block rounded-md"
                    type="text"
                    value={title}
                    onChange={(e) => setTitle(e.target.value)}
                    placeholder="Enter article title"
                />
                <textarea
                    className="w-full px-4 pt-4 block rounded-md"
                    name="post-body"
                    id="post-body"
                    value={body}
                    onChange={(e) => setBody(e.target.value)}
                    placeholder="Enter article body. you can use markdown syntax!"
                />
                <button
                    title={!body || !title ? "Fill form completely" : "Submit form"}
                    disabled={!body || !title}
                    className="block rounded mt-8"
                    type="submit"
                >
                    Submit Post
                </button>
            </form>
        </div>
    );
};
export default WriteAnArticle;
```


If done correctly, our application should look like the image below 


![Form to edit or submit an article/post in our app](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f3wbjhys8belwp83z1e3.png)


## Concluding our Application

To conclude our application, navigate to your `App.tsx` file in our `src` folder and add the code snippet below


```jsx
import { BrowserRouter, Switch, Route } from "react-router-dom";
import Navbar from "./components/Navbar/Navbar";
import Post from "./pages/post";
import Homepage from "./pages/home";
import WriteAnArticle from "./pages/write";
import Profile from "./pages/profile";
const App = () => {
    return (
        <>
            <BrowserRouter>
                <Navbar />
                <Switch>
                    <Route path="/" exact component={Homepage} />
                    <Route path="/article/:id" exact component={Post} />
                    <Route path="/write-article" exact component={WriteAnArticle} />
                    <Route path="/edit-article/:id" exact component={WriteAnArticle} />
                </Switch>
            </BrowserRouter>
        </>
    );
};
export default App;
```


The code above defines routes for all pages in our app, 

- `/` will navigate to the homepage
- `/article/:id` will navigate to a Post page where we view a post completely. It’s a dynamic route and we will get the id from our url in the Post page.
- `/write-article` will navigate to write article page
- `/edit-article/:id` will take us to edit post page

We also added our Navbar component, if done correctly our application homepage should look like the image below.


![React markdown](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2l8hf4ik0fs6ovipgyez.png)

You can now go ahead and test the app with markdown syntax and extend it if you want. 


## **Conclusion**

In this tutorial, we looked at markdown, react-markdown, we learned how to install and render markdown safely with React markdown. We also reviewed how to use `remark-gfm` to extend react-markdown features and also how to make markdown styles display correcty in apps using tailwind.

You can learn more by looking at react markdown’s official [documentation](https://remarkjs.github.io/react-markdown/).