An elegant way to deal with rich text fields in React.
Time and time again I will run into the following problem: the CMS gives us plain HTML from a rich text field to use in our React app.
Sure, we can just render a div
and pass dangerouslySetInnerHtml
to it, but this leaves us with quite a few caveats:
- Clicking on an
a
tag that links to an internal page causes a full page load.
In SPA setups this is not behavior we want, we use fancy frameworks (next, gatsby, etc) to do all sorts of preloading and client side fetching, and doing a full page load negates all of that.
It will definitely downgrade the user experience. - Having to deal with styling every tag.
You now have yourdiv
with rich text HTML inside it, but what about it's children?
You will likely need to style those in one way or another.
You can setup some additional CSS to target these, but because it's just content, you're probably now doing double the work because you likely already have generic components for your headings, links, text bodies, images, etc. If only there was a way we could just reuse the components we're already using internally? - Congratulations, you now have an XSS exploit!
Ever wondered whydangerouslySetInnerHtml
starts with... dangerously? It's (and I quote) "because it’s easy to inadvertently expose your users to a cross-site scripting (XSS) attack".
Meaning if someone managed to hijack our CMS and inject all sort of unwanted tags (like the<script>
tag), they would have full access to the HTML that gets rendered on our site.
Making it possible to deface the website, steal session info, redirect to another website altogether, and all sorts of possible nastyness. - The CMS has full control of what it gets to place on your site.
Eons after delivering a project you decide to check out the site, only to find out the client has decided to swap out it’s fonts for a nice Comic Sans with a lovely bright blue shade that’s absolutely unreadable and goes against everything you stand for.
While luckily these days I mostly work with much more configurable WYSIWYG editors, some of us aren’t that lucky and have to deal with the cards we’re dealt, while still wanting a way to protect clients from themselves.
The search for something more declarative
I ran into react-html-parser, which conceptually actually mostly is what I’m looking for, but it was still too functional and low level in it’s actual usage. Because I’m used to ‘thinking in react’, I wanted something more declarative.
Now consider the following:
All we need to use this component is provide the HTML string itself, and a simple mapping from tag names to our own components.
- If we only want to provide full passthrough from one HTML tag to a component, we simply provide the component itself.
- If we (for example) want to provide additional props besides the HTML attributes, we can simply write a small wrapper component (in the cases of
h1
throughh5
in the above example). - We’ve now also now solved the common internal links problem by writing another small wrapper component that checks if URL’s start with
/
, and if so, will render them through our router’sLink
component instead.
Additionally, if we’re using typescript, we get full autocompletion for the attributes of the specified HTML element.
Composability
Take the following example:
We have an article entry in our CMS, that has the following (rich) fields:
a ‘intro’ field, and a ‘text’ field containing the actual article content text.
The intro’s paragraphs will need to be displayed in a slightly larger font (to differentiate it from the article contents).
Now we’ll have to mess around with styling the intro’s wrapping container and attempting to target the paragraphs inside it through CSS right? Wrong!
We can just reuse the mapping we’re already using for the article’s text, but override the value for p
:
Security & predictability
If our HTML contained any tags that aren’t in our mapping, they will be ignored! This is what you will likely generally prefer, because your app is designed (both from a design and a software point of view) to only support certain tags.
If the text editor decides to throw something funny in there (script tags…), it’s generally best left ignored.
However, we might have some tags that we want to render ‘as-is’: In that case instead of providing a component, we can pass null
.
Now, it will not be ignored anymore, but you will still have a whitelist of ‘allowed’ tags.
Alternatively, if you wish to have this behavior apply to all elements that aren’t in our tag mapping, there’s the acceptUnknown
prop: