React ContentEditable Component
react-contenteditable is a React component that simplifies the integration of the native `contenteditable` HTML attribute into React applications. It provides a controlled component approach, allowing developers to manage the `innerHTML` of an editable HTML element (like a `div` or `article`) via React state. The package, currently at version 3.3.7, addresses many common challenges associated with `contenteditable` in React, such as cursor jumping and state synchronization, often encountered when directly managing DOM mutations. It abstracts the use of `dangerouslySetInnerHTML`, providing a safer and more idiomatic React interface. While the component is designed to mitigate typical `contenteditable` pitfalls, users should be aware of inherent complexities like input sanitization to prevent XSS attacks and managing rich text pasting. The library ships with TypeScript types.
Common errors
-
Warning: A component is contentEditable and contains children managed by React.
cause React warns about potential conflicts when its virtual DOM manages children within an element whose content can also be directly edited by the browser via `contenteditable`.fixIf you are intentionally managing the content via `react-contenteditable`'s props, you can suppress this warning by adding `suppressContentEditableWarning={true}` to the `ContentEditable` component. -
TypeError: Cannot read properties of null (reading 'current')
cause The `innerRef` property was accessed before it was properly assigned to a DOM element, often due to incorrect initialization or timing, especially with functional components.fixEnsure `innerRef` is initialized with `React.createRef()` in class components or `useRef(null)` in functional components, and that the ref is only accessed after the component has mounted (e.g., within `useEffect` with an empty dependency array or after render in class components). -
Contenteditable doesn't work properly with React State (writes in the opposite way) / Cursor jumps to the end / Input state out of sync
cause Direct use of `useState` with `ContentEditable` causes frequent re-renders that interfere with the browser's native cursor positioning and input handling within the editable element.fixRefactor to use `useRef` to store the HTML content. Update `text.current` within the `onChange` handler and pass `text.current` to the `html` prop. The `ContentEditable` component will then manage the DOM updates more gracefully without triggering unwanted React re-renders for every keystroke.
Warnings
- gotcha Using React's `useState` hook directly to manage the content of `<ContentEditable />` often leads to unexpected behavior, cursor jumping, or issues with input synchronization.
- gotcha When integrating `contenteditable` elements in React, you might encounter the console warning: '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.'
- breaking Unsanitized HTML input into `contenteditable` elements, particularly when using `dangerouslySetInnerHTML` (which `react-contenteditable` abstracts), poses a significant cross-site scripting (XSS) vulnerability. Attackers could embed malicious scripts.
- gotcha Pasting rich text content directly into a `contenteditable` element often retains original formatting, leading to inconsistent styles or unwanted HTML tags being introduced, instead of plain text.
Install
-
npm install react-contenteditable -
yarn add react-contenteditable -
pnpm add react-contenteditable
Imports
- ContentEditable
import { ContentEditable } from 'react-contenteditable'import ContentEditable from 'react-contenteditable'
- ContentEditable
const { ContentEditable } = require('react-contenteditable')const ContentEditable = require('react-contenteditable')
Quickstart
import React from 'react';
import ContentEditable from 'react-contenteditable';
class MyEditor extends React.Component {
constructor() {
super();
this.contentEditable = React.createRef();
this.state = { html: "<p><b>Hello</b> <i>World</i></p>" };
}
handleChange = (evt) => {
this.setState({ html: evt.target.value });
};
// Optional: Sanitize content on blur to prevent XSS or unwanted formatting
handleBlur = () => {
console.log('Final content:', this.state.html);
// In a real app, you might sanitize or save the content here
// E.g., this.setState({ html: sanitizeHtml(this.state.html) });
};
render() {
return (
<ContentEditable
innerRef={this.contentEditable}
html={this.state.html} // innerHTML of the editable div
disabled={false} // use true to disable editing
onChange={this.handleChange} // handle innerHTML change
onBlur={this.handleBlur}
tagName='article' // Use a custom HTML tag (uses a div by default)
suppressContentEditableWarning={true} // Suppress React's contentEditable warning
style={{ border: '1px solid #ccc', minHeight: '100px', padding: '10px' }}
/>
);
}
}
export default MyEditor;