In my last post, I went over how I created a contenteditable div
in Ember. This lead me to want to see how I'd do it in React.
My First Implementation with a Jumping Cursor
I originally started with the following implementation, adhering to a controlled component data flow:
import { useState } from "react";
export default function App() {
const [content, setContent] = useState("foo");
return (
<div
contentEditable="true"
onInput={event => {
setContent(event.target.textContent);
}}
>
{content}
</div>
);
}
The first problem is that there is the following warning in red in the console:
Warning: A component is
contentEditable
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 second problem is that as a user types into the contenteditable div
, the cursor jumps to the beginning of the element.
The Solution
To fix this, I started by creating a component called Contenteditable
that can be used as follows:
import { useState } from "react";
import Contenteditable from "./Contenteditable";
export default function App() {
const [content, setContent] = useState("foo");
return (
<>
<Contenteditable
value={content}
onChange={updatedContent => {
setContent(updatedContent);
}}
/>
<p>{content}</p>
</>
);
}
Here is the implementation of the Contenteditable
component:
import { useEffect, useRef } from "react";
export default function Contenteditable(props) {
const contentEditableRef = useRef(null);
useEffect(() => {
if (contentEditableRef.current.textContent !== props.value) {
contentEditableRef.current.textContent = props.value;
}
});
return (
<div
contentEditable="true"
ref={contentEditableRef}
onInput={event => {
props.onChange(event.target.textContent);
}}
/>
);
}
Updating the textContent
of the div
causes the cursor to jump to the beginning, so to prevent this from happening on every keystroke, I conditionally assign the textContent
only if it isn't the same as the value
prop. The only time these two values might not be the same is on initial render where I specified an initial value of "foo" and the element's initial textContent
is an empty string. Even if there isn't an initial value, and hence the values will be the same (an empty string), there won't be a problem as the cursor will just start at the beginning of the element. As a user types, these two values will be the same so the textContent
won't get reassigned and thus the cursor won't jump to the beginning. Problem fixed!