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.

Adding Editable HTML Content to a React App
Photo by Thought Catalog / Unsplash

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.

React: Up & Running: Building Web Applications from Amazon

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 is contentEditable and contains children 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!