question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

try to prevent re-rendering at the Leaf level

See original GitHub issue

Do you want to request a feature or report a bug?

Idea/debt.

What’s the current behavior?

Right now, we re-render the DOM in Slate with every change that occurs in the editor. This is different from Draft.js (which also uses React), which aborts re-rendering for “simple” changes to the DOM, like inserting or deleting characters.

Our current approach has a lot of benefits in terms of simplicity…

  • It makes it easy to ensure that schema validations can be applied even at the text level. Since all changes will result in re-rendering, we don’t have to differentiate and handle the “simple” changes in a more complex way to account for them already having been applied to the DOM, but now needing to be normalized.

  • It makes it possible to create editors which render elements that update for any change, even basic ones like inserting text. Since every changes results in a re-render, plugins can render things like word counters, or block-specific styles, easily without having to account for the fact that some changes don’t actually trigger renders.

  • It makes all of the logic in the Before/After plugins a lot easier to follow, since they don’t have to reverse engineer what the specific changes the browser may or may not have made in the contenteditable element out from under React.

However, even right now we’re not doing it for 100% of DOM operations. We currently don’t do it for spellchecking, since those changes are applied to the DOM immediately, without going through a usable event (at least in most browsers).

But this “always re-render” approach also has some downsides…

  • It makes old browser and mobile browser support harder. Since these browsers use a system where the beforeinput event can’t be prevented, preventing it makes it hard (and/or impossible) to get Slate to work in these browsers. https://github.com/ianstormtaylor/slate/issues/2047 https://github.com/ianstormtaylor/slate/issues/1720

  • It makes IME support harder? Unsure of this one. But since IME is also a case where we can’t actually prevent the defaults, since we need to read the text in the DOM, this might be a similar situation.

  • It makes spellcheck glitchier, since browsers blink when text is re-rendered before they spellcheck it again. https://github.com/ianstormtaylor/slate/issues/1934

  • It’s less performant, in the case of simple insertions. Not terribly so, and for most editors this isn’t a bottleneck, but it does require re-rendering the DOM even for “simple” cases like inserting a single character. This is not a big reason for doing this, since there are many other places that would be important to improve first for this.

So, where does that leave us…

What’s the expected behavior?

First, a look at what the render tree looks like for Slate:

<UsersApp> (userland)
  <Editor>
    <Content>
      <Node>
        <UsersNode> (userland)
          <Text>
            <UsersMark> (userland)
              <Leaf>
      <Node>...
      <Node>...

The important thing that differentiates Slate from other React libraries/frameworks is that it allows for “userland” islands of rendering inside its own rendering layers. For this reason, if you use shouldComponentUpdate to abort rendering at the <Editor> level, the <UsersNode/UsersMark> userland levels will not re-render, breaking expectations.

We used to actually do the same thing as Draft.js, and let the DOM be updated by the browser for “simple” changes, and then aborting our own rendering. However, as mentioned above, this prevents us from doing certain things, because it aborts rendering at the <Content> level in the diagram above, which means that the <UsersNode> (userland) never gets re-rendered.

We removed this logic a long time ago, because we thought it was necessary to allow for user-defined schemas to be validated and to allow for more complex node rendering behaviors. It was before we had Operation level semantics. But also because we were able to memoize the Immutable.js structures, which meant it was no longer a required case for performance.

We might need to bring it back in some form though, for mobile support, IME support, and graceful spellcheck support.

Instead, I think it may be possible to re-render at the <Editor> level for each change, but abort rendering at the <Leaf> level, which is the lowest component Slate renders.

This would be different from our old approach in that it would allow us to gain the “always re-renders” benefits higher up the tree, so that custom nodes can still have total flexibility in what they render. But it would hopefully give us the benefits of “selective re-rendering” that come from allowing the browser’s native DOM editing to occur.

It would preserve one of the constants that (I think) is required for Slate’s flexibility, which is that userland can always count on the editor re-rendering as if the DOM did not exist. (Kind of like the same tenet React offers for the regular DOM, but for contenteditable too.)


I think the way to do this best might be to add an isNative flag (like we used to have on Value objects) to Operation objects instead. This way we might be able to consider in <Leaf> nodes whether or not to re-render if all of the operations reference the leaf and are native, then abort rendering.

The newly added paths can also help because operations are path-based, and can hopefully be directly mapped to the leaves in the tree.

This is just an idea, I’d love others’s thoughts if you see any issues. It will definitely result in more complexity in core, but hopefully it unlocks some compatibility.

Issue Analytics

  • State:open
  • Created 5 years ago
  • Reactions:12
  • Comments:7 (5 by maintainers)

github_iconTop GitHub Comments

1reaction
thesunnycommented, Aug 8, 2018

@ianstormtaylor Just thinking this through and how we might handle the Android issues.

I wonder if we need a concept in here to do with uncertainty and finalization.

For example, autosuggest, autocorrect and IME may result in Slate not being able to predict what is in the DOM. So as we type characters, we aren’t sure what is in the DOM. Then, at some point, we finalize our entry and then we have to reconstruct the actual operation from what is in the DOM.

According to nathanfu in this document https://docs.google.com/document/d/1Hex89Di-r-Wfpo1DLAtxpetoX588ziXVoNyC87Je3Xc/edit# it doesn’t seem likely that we can predict DOM state reliably across all versions of mobile Android browsers using events. Maybe at some point, old versions disappear and new API implementations may be able to get us there but for now, I think it’s impossible.

In order for this to work with collaborative editing, I presume we would have to freeze incoming operations until a finalize event occurs.

Maybe it works something like this across browsers for typing “Hello” with some sort of autosuggest that can complete after “He” so in most browsers we get:

// Note: Pseudocode. Probably not the actual operations...
==> keydown "H"
{op: 'insert', value: 'H', isNative: true}
==> keydown "e"
{op: 'insert', value: 'e', isNative: true}
==> autocomplete: "Hello"
{op: 'insert', value: 'llo', isNative: true}

The above assumes in most browser we are able to reliably predict state through events.

In Android without reliable prediction we get

==> keydown "H"
{op: 'entry', isNative: true}
==> keydown "e"
{op: 'entry', isNative: true} // a noop
==> autocomplete "Hello"
{op: 'finalize', isNative: true}
==> Slate intercepts the `finalize` and then the history is rewritten to:
{op: 'insert', value: "Hello"}

During finalize Slate reads the DOM in order to reconstruct the actual op. What do you think?

I think the other idea that goes along with this is that entry and finalize are ops that do not get sent during collaborative editing. We wait for finalize to fix the op to an insert and then that gets sent.

0reactions
ianstormtaylorcommented, Aug 15, 2018

@zhujinxuan I think it’s unclear right now which native beforeinput events are actually cancellable. Safari’s are almost all cancellable. Chrome’s supposedly are not, but I’ve seen insertText events be cancelled fine. And then supposedly none of the beforeinput events fired during compositions are cancellable.

Would need someone to research it.

Read more comments on GitHub >

github_iconTop Results From Across the Web

How to stop re-rendering lists in React? - Alex Sidorenko
First, let's simplify our example by removing all props from the Item . We will still update the parent state but won't pass...
Read more >
Prevent Re-rendering inside a map React - Stack Overflow
I think this problem is because im rendering inside a map, but im looking for avoid that for the performance... I tried with...
Read more >
Rendering - Slate
It tries to keep everything as React-y as possible. ... Leaves. When text-level formatting is rendered, the characters are grouped into "leaves" of...
Read more >
Just Say No to Excessive Re-Rendering in React - GrapeCity
Read on to learn more about re-rendering and how to avoid excessive ... used components up into a parent (or even top-level) component....
Read more >
5 things not to do when building React applications
If the numbers are significant, you can optimize them by preventing rerendering of the unaffected pure functional components.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found