4

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.

Code Sandbox with example

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>
  );
}
Community
  • 1
  • 1
cbdeveloper
  • 27,898
  • 37
  • 155
  • 336
  • 1
    I think you should takeout the java tag and put the javascript tag instead. – Lungu Daniel Apr 23 '19 at 13:43
  • 1
    @LunguDaniel Sure! Thanks. It did not autocomplete my string. My bad. – cbdeveloper Apr 23 '19 at 13:45
  • Hi @cbdev420, were you able to solve the issue? We currently have the same in one of your projects. Thanks for your help. – natterstefan Jul 17 '19 at 11:14
  • @natterstefan The solution I've ended up following was to keep the content editable div uncontrolled (without state). So the user can freely edit. You can still use the `onInput` method to keep and update the text value of the `div` inside a `useRef` variable so you can perform some logic on it. Note: In the end, I gave up completely on content editable `div`s, since there are no standards across different browsers on what the `html` of the text should be. Too messy for what I was trying to do. – cbdeveloper Jul 17 '19 at 12:16
  • Thanks for the answer @cbdev420. That's what I experienced as well. We gave up using `contentEditable` as well and use https://github.com/buildo/react-autosize-textarea now. It's sufficient in our current use case. Thanks. – natterstefan Jul 18 '19 at 08:17

0 Answers0