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.

feat(slate-react): more flexible / performant dynamic decorations via small API change

See original GitHub issue

Problem With the existing decorations mechanism, it is not easy to make decorations dynamic, so they’re recomputed based on external state. Currently the only way to do it is to change the decorate function, which causes every element in the document to re-render. (There was a recent change #4138 which improved this slightly by keeping the decorate function in a context rather than passing it down as a prop, but still every Element component accesses the context.)

For example: In my application I use decorations for error highlighting in code blocks. A change in one code block can affect highlighting in another block, so I need to redecorate on every edit to a code block. Changing the decorate function on even a moderately-sized document is very slow.

Solution A small change to the renderElement API can greatly improve the situation: pass a new renderChildren argument, which takes a decorations argument. These decorations are added to any existing decorations when rendering the children.

Now in renderElement, instead of e.g.

const renderElement = (props: RenderElementProps) => {
  switch (props.element.type) {
    case 'code':
      return <code>{props.children}</code>
    ...

we can write

const renderElement = (props: RenderElementProps) => {
  switch (props.element.type) {
    case 'code':
      const decorations = ... // syntax highlighting
      return <code>{props.renderChildren({ decorations })}</code>
    ...

We can use whatever React state mechanism we like to trigger re-rendering / redecoration per node, rather than globally.

As a side benefit: with the existing decoration mechanism, the decorate function is run on the immediate children of the editor on every edit (in useChildren in Editable), even when the function is not changed. This doesn’t cause the elements to be re-rendered (because Element is memoized on the decorations list), but it can be expensive in itself (in my test case it was ~20ms). If we compute decorations per node in renderElement, they are not recomputed on every edit.

I prototyped this change in my app and it makes an enormous difference—in a moderately-sized document, editing a code block drops from ~200ms to ~10ms.

Alternatives I experimented with rendering the decorations manually in renderElement. Unfortunately I wasn’t able to get this to work because Slate needs Text nodes to be rendered with the Text component (to populate various WeakMaps used to map DOM elements to Slate nodes), and this component is not exported. Even if this did work, the decorations mechanism is nicer—it requires less code duplication, exposes less of Slate’s internals, and implements cross-node decorations already.

For backward compatibility, it’s necessary still to pass children as well as renderChildren to renderElement. This can be made performant by making children lazy, so we don’t do unnecessary work if renderChildren is used instead.

For cases where we want to decorate Text nodes directly (rather than decorating a parent Element), I think it would make sense to add a renderText handler to Editable, so there is opportunity to pass decorations.

I think the approach of passing decorations to renderChildren is better overall than a global decorate function, and in some future revision it would make sense to remove decorate entirely.

Context PR forthcoming.

Issue Analytics

  • State:open
  • Created 2 years ago
  • Reactions:16
  • Comments:6 (5 by maintainers)

github_iconTop GitHub Comments

2reactions
jakedcommented, Sep 2, 2021

I’m going to experiment with calling React hooks from the existing decorate function (see comments on PR #4484) to see if we can achieve the goal without changing the API.

0reactions
bryanphcommented, Nov 17, 2021

@jaked I’m not sure what you mean by reactive but isn’t that kind of what RangeRef provides for this kinda case? Those are kept up-to-date with the editor state.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Rendering - Slate
However, decorations are computed at render-time based on the content itself. This is helpful for dynamic formatting like syntax highlighting or search keywords ......
Read more >
when updating value of Editor with api returned data it doesn't ...
when updating value of Editor with api returned data it doesn't update or retender to show that data in editor, was working fine...
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