feat(slate-react): more flexible / performant dynamic decorations via small API change
See original GitHub issueProblem
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:
- Created 2 years ago
- Reactions:16
- Comments:6 (5 by maintainers)

Top Related StackOverflow Question
I’m going to experiment with calling React hooks from the existing
decoratefunction (see comments on PR #4484) to see if we can achieve the goal without changing the API.@jaked I’m not sure what you mean by reactive but isn’t that kind of what
RangeRefprovides for this kinda case? Those are kept up-to-date with the editor state.