I'm currently building a component with a contenteditable div
, using React. Since the content is generated by the user, I was trying to keep my component state
in sync with its contents by using dangerouslySetInnerHTML
. My other option was to update the innerHTML
property directly.
I thought it would be important to use dangerouslySetInnerHTML
, based on the following answer of this other SO question:
React.js: Set innerHTML vs dangerouslySetInnerHTML
Yes there is a difference!
The immediate effect of using innerHTML versus dangerouslySetInnerHTML is identical -- the DOM node will update with the injected HTML.
However, behind the scenes when you use dangerouslySetInnerHTML it lets React know that the HTML inside of that component is not something it cares about.
Because React uses a virtual DOM, when it goes to compare the diff against the actual DOM, it can straight up bypass checking the children of that node because it knows the HTML is coming from another source. So there's performance gains.
More importantly, if you simply use innerHTML, React has no way to know the DOM node has been modified. The next time the render function is called, React will overwrite the content that was manually injected with what it thinks the correct state of that DOM node should be.
But now I've come up with the following issue:
When there's a new input and you set the state with the new Html, your component is re-rendered withe the new dangerouslySetInnerHTML={{ __html: html }}
and your caret jumps position (goes back to the beginning of the line on all browsers tested so far: Chrome, Firefox, Edge).
But there's nothing new here. This (caret jump) would also happen if I'd set the new html using innerHTML
. The the problem with dangerouslySetInnerHTML
receiving state
with the new html
is that when you set the new state
, it's an asynchronous call on setState()
.
So you can't call placeCaret()
right away. It only works if you use setTimeout()
to allow some time to pass for the new state
to update the component.
setHTML(root.innerHTML);
setTimeout(() => placeCaret(root.innerText.length), 100); // TIMEOUT NEEDED
//placeCaret(root.innerText.length);
Anyway, this is a workaround but it ends up in a terrible UX, because the user can see the caret jumping on every key stroke.
QUESTION
Is there a solution for this caret jumping situation using dangerouslySetInnerHTML
? Or should I just avoid using it and go straight to manipulating the innerHTML
so I can get immediate changes on my element?
NOTE: I don't think that preventing component update using shouldComponentUpdate
is a viable solution. Because sometimes I'll actually have to modify the html, like, adding some text into a span, etc. So I'll need the component update without the caret jumping.
import React, { useRef, useState } from "react";
import ReactDOM from "react-dom";
import styled from "styled-components";
const S = {};
S.div = styled.div`
border: 1px dotted blue;
`;
function App() {
const divRef = useRef(null);
const [html, setHTML] = useState("<div><br></div>");
function onInput() {
const root = divRef.current;
setHTML(root.innerHTML);
setTimeout(() => placeCaret(root.innerText.length), 100);
//placeCaret(root.innerText.length);
}
// PLACE CARET BACK IN POSITION
function placeCaret(position) {
const root = divRef.current;
const selection = window.getSelection();
const range = selection.getRangeAt(0);
range.setStart(root.firstChild.firstChild, position);
}
function onKeyDown(e) {
console.log("On keydown... " + e.key);
if (e.key === "Enter") {
e.preventDefault();
}
}
return (
<React.Fragment>
<S.div
contentEditable
ref={divRef}
onInput={onInput}
onKeyDown={onKeyDown}
dangerouslySetInnerHTML={{ __html: html }}
/>
</React.Fragment>
);
}