Adding Editable HTML Content to a React App
In this post, we'll look at how contentEditable can be used with React to enable interactive, editable content in you React apps.
Have you ever wondered how apps like Google Docs or Notion pull off interactive, editable rich content in web apps? The usual approach to building forms with inputs like textarea
can't deliver the same kind of experience.
Instead, apps like these make use of HTML's contentEditable
attribute, which allows a user to directly edit content on a page. While enabling content editing is as simple as setting the contentEditable
attribute though, actually integrating it into an app and getting usable data out of it is a different story.
In particular, if you've tried using contentEditable in React, you'll quickly realize that the two don't play nicely together–at least, not without a bit of extra work. In this post, we'll look at how contentEditable
can be used with React to enable interactive, editable content in you React apps.
What is contentEditable?
contentEditable
is an attribute that can be placed on HTML elements to make the content editable by the user. We can demonstrate how contentEditable
is used with the following HTML snippet:
<div style="border: 1px solid #ccc; padding: 8px" contentEditable>
<h4>Editable Content - Edit Me!</h4>
<p>This piece of content has the contentEditable attribute set.</p>
</div>
That HTML block is embedded below to show how it works in action. Go ahead, edit the content in the block below!
Editable Content - Edit Me!
This piece of content has the contentEditable attribute set.
As mentioned earlier, enabling editing with contentEditable
is simple–but it's not always easy to make it work with your app. To fully build an app with contentEditable
, you need to address the following issues in one way or another.
- Add change handlers for the updated content
- Add validation and sanitization of the edited content
- Add translation to-and-from your internal data representation (most apps do not store raw HTML for the content)
- Make content editable play nicely with the rest of your app's data lifecycle
This last point is a particular challenge with React–contentEditable
and React conflict in how they manage and update DOM state.
What are the challenges using contentEditable with React?
Before looking at contentEditable
with React, let's quickly look at how we might implement non-rich text editing with textarea
:
const TextAreaEditable = () => {
const [content, setContent] = React.useState("")
return (
<textarea
value={content}
onChange={e => setContent(e.currentTarget.value)}
/>
)
}
But if we try to follow this model for contentEditable
content in a React app, we'll quickly start running into errors, warnings and unexpected behaviors. Let's take a look at a naive attempt at adapting the code above for contentEditable
:
🚫 Our naive attempt at using contentEditable:
const Editable = () => {
const [content, setContent] = React.useState("")
return (
<div onBlur={t => setContent(t.currentTarget.innerHTML)}
contentEditable>
{content}
</div>
)
}
If we look in the console, we see this warning as soon as we render the page:
Warning: A component iscontentEditable
and containschildren
managed by React. It is now your responsibility to guarantee that none of those nodes are unexpectedly modified or duplicated. This is probably not intentional.
The problem is that with contentEditable
enabled, both React and the browser want to handle the state of the content inside of the element. Additionally, as we start to edit the content, we'll run into some other problems.
If I start editing the content, and hit command-B to bold some of the text I'm editing, I'll notice two things:
First of all–it works!–without doing any special handling, we're able to do the kind of text style manipulation we'd expect from an interactive editor.
But the second thing I noticie is that when the view blurs and the state update occurs, my text gets transformed into a mess of HTML! Instead of seeing the text "123", I see 1<span style="font-weight: bold;">2</span>3
That's not what we want!
The issue, of course, is that our content string is pure HTML, but we're trying to render it in React by passing it as a child of div
. React does not interpret those children as HTML and escapes the content instead.
A Dangerous Solution
To make our contentEditable
component work correctly, we need to tell React to render an HTML string, instead of plain text. Fortunately, such a technique exists, but it has a scary sounding name: dangerouslySetInnerHTML
.
The dangerouslySetInnerHTML
attribute can be used to set the inner HTML of an element, letting us rewrite our Editable
component:
✅ Our second attempt at using contentEditable:
const Editable = () => {
const [content, setContent] = React.useState("")
const onContentBlur = React.useCallback(evt => setContent(evt.currentTarget.innerHTML))
return (
<div
contentEditable
onBlur={onContentBlur}
dangerouslySetInnerHTML={{__html: content} />
)
}
As the name suggests, dangerouslySetInnerHTML
is dangerous. Why? Because unsanitized HTML input can be a security risk and expose users to cross-site scripting (XSS) attacks. Imagine we're using contentEditable
to enable collaboration between users. If we're not careful, an attacker could embed JavaScript in the content that will get executed in the victim's browser when dangerouslySetInnerHTML
is used.
Because of this, it's always recommended to use some form of input sanitization when using contentEditable
and dangerouslySetInnerHTML
. The package sanitize-html
can help enforce a set of whitelisted HTML tags to prevent XSS attacks.
✅ Sanitized HTML with contentEditable:
import sanitizeHtml from "sanitize-html"
const Editable = () => {
const [content, setContent] = React.useState("")
const onContentBlur = React.useCallback(evt => {
const sanitizeConf = {
allowedTags: ["b", "i", "a", "p"],
allowedAttributes: { a: ["href"] }
};
setContent(sanitizeHtml(evt.currentTarget.innerHTML, sanitizeConf))
}, [])
return (
<div
contentEditable
onBlur={onContentBlur}
dangerouslySetInnerHTML={{__html: content}} />
)
}
Note that to be sure your application isn't subject to XSS attacks, it's not enough to only validate during editing. You also need to sanitize and validate content on the server-side or on the content coming back from the server! If you're only sanitizing when a user edits the content, a clever attacker can bypass the safety checks and send unsafe data directly to the server.
How to Use react-contenteditable
While the basic approach above lets you use contentEditable
in your React app, you may notice some edge cases as you start to implement features in your app. Specifically, if you add additional event handlers to your div
to capture input before the blur event, you might see cursor jumping and other undesirable behaviors. That's because a bit of additional logic is needed to determine when to actually modify the content state and trigger a re-render.
Though we could add a workaround for this and other edge cases in our component without too much trouble, why re-invent the wheel? The react-contenteditable
package solves most of the common issues you're likely to encounter with contentEditable
in a React app.
Using this package is a pretty simple change from our previous example:
✅ Sanitized HTML with the ContentEditable package:
import sanitizeHtml from "sanitize-html"
import ContentEditable from 'react-contenteditable';
const Editable = () => {
const [content, setContent] = React.useState("")
const onContentChange = React.useCallback(evt => {
const sanitizeConf = {
allowedTags: ["b", "i", "a", "p"],
allowedAttributes: { a: ["href"] }
};
setContent(sanitizeHtml(evt.currentTarget.innerHTML, sanitizeConf))
}, [])
return (
<ContentEditable
onChange={onContentChange}
onBlur={onContentChange}
html={content} />
)
}
By using sanitize-html
and react-contenteditable
, we can quickly build the basics of rich, interactive content editing in a React app.
Wrap-up
In this post, we've learned a bit about how to add useer editable content to your React app. The main considerations are:
contentEditable
in React requires some special handling.- The
react-contenteditable
package can help handle edge cases you might encounter. - Remember to sanitize HTML both when editing and before rendering untrusted content. The
sanitize-html
package can help with this.
Using this simple approach, you can start adding editable content to any React app!