Back

useMarked hook

Filipe Herculano, February 11, 2020

random image from unsplash

I am making a side project in React that requires markdown parsing so I decided to use that as a good candidate to experiment with custom hooks

Checkout this fantastic post from Amelia Wattenberger that goes over a comparison between traditional class components versus using hooks and how they make React feel less bloated and more natural to work with

Here's what I needed to do:

  • Parse a markdown string
  • Sanitize that string to prevent XSS attacks

Apparently, there is a vast number of parsers out there, I decided to go with marked which seems like a good library with an active community and a nice and simple implementation

Again, the same could be said for sanitizing html (for some reason people just like writing parsers a lot), so I picked sanitize-html which offers a nice level of configuration through a simple object

Setup

Alright let's get to work

// parsing markdown with marked
const marked = require('marked')
const md = `
  # heading

  [link][1]

  [1]: #heading "heading"
`

const tokens = marked.lexer(md)
const html = marked.parser(tokens)

Will output this html!

<h1 id="heading">heading</h1>
<p><a href="#heading" title="heading">link</a></p>

Now, to prevent XSS, let's add this before using the html

// sanitizing raw html with sanitize-html
const sanitizeHtml = require('sanitize-html')
// passing the html output from marked
const clean = sanitizeHtml(html)

Output now is

heading
<p><a href="#heading" title="heading">link</a></p>

Wait, what? Where's our h1 tag? Well, apparently the default options for sanitize-html consider h1 unsafe (I guess), they go over the specs in their README so I went and added my custom defaults which looks like this

View defaults
{
  "allowedTags": [
    "h1",
    "h2",
    "h3",
    "h4",
    "h5",
    "h6",
    "blockquote",
    "p",
    "a",
    "ul",
    "ol",
    "nl",
    "li",
    "b",
    "i",
    "strong",
    "em",
    "strike",
    "code",
    "hr",
    "br",
    "div",
    "table",
    "thead",
    "caption",
    "tbody",
    "tr",
    "th",
    "td",
    "pre",
    "iframe"
  ],
  "disallowedTagsMode": "discard",
  "allowedAttributes": {
    "a": ["href", "name", "target"],
    "img": ["src"]
  },
  "selfClosing": [
    "img",
    "br",
    "hr",
    "area",
    "base",
    "basefont",
    "input",
    "link",
    "meta"
  ],
  "allowedSchemes": ["http", "https", "ftp", "mailto"],
  "allowedSchemesByTag": {},
  "allowedSchemesAppliedToAttributes": ["href", "src", "cite"],
  "allowProtocolRelative": true
}

Marked also supports a nice set of configurations (syntax highlighting being my favourite) you can checkout their docs here

useMarked('# yay!')

Awesome, we have everything, let's turn that into a React hook called useMarked

import { useState, useEffect } from 'react'
import sanitizeHTML from 'sanitize-html'
import marked from 'marked'

import defaultOptions from './defaultOptions'

export const useMarked = (markdown, options = defaultOptions) => {
  const [html, setHtml] = useState(markdown)

  useEffect(() => {
    if (options.markedOptions) {
      marked.setOptions(options.markedOptions)
    }
    const tokens = marked.lexer(markdown)
    const html = marked.parser(tokens)
    setHtml(
      options.skipSanitize ? html : sanitizeHTML(html, options.sanitizeOptions)
    )
  }, [markdown])

  return html
}

And now we can use it in any function component by doing

import React from 'react'
import { useMarked } from 'use-marked-hook'

const App = () => {
  const markdown = `**bold content**`
  const html = useMarked(markdown)
  // html -> <p></strong>bold content</strong></p>
  return <div dangerouslySetInnerHTML={{ __html: html }} />
}

Testing Custom Hooks

I also found that there's a quick way to test your hooks using the @testing-library/react-hooks package which provide us with the nice renderHook helper

Testing our useMarked hook looks like this

import { useMarked } from 'use-marked-hook'
import { renderHook } from '@testing-library/react-hooks'

describe('useMarked', () => {
  it('Receives markdown and returns html', () => {
    const { result } = renderHook(() => useMarked('# test'))
    expect(result.current).toBe('<h1>test</h1>\n')
  })
})

⚠️ Note the newline character added at the end of the output (jest errors were very unhelpful in seeing that and it took me quite a bit to realize tests were failing because of it 🤦‍♂️)

Conclusion

To save you some effort, if you ever find the need for a markdown parser in your react projects, I published this custom hook as an npm package which you can download and use now 😉

yarn add use-marked-hook

I made the code for it available on github

It also includes a sample react app that uses useMarked hook to render a local markdown file into an html page that is later published live through github pages, checkout the result here