draft-js icon indicating copy to clipboard operation
draft-js copied to clipboard

How to force re-render of Editor

Open thesunny opened this issue 8 years ago • 31 comments

Does anybody know how to force a re-render of the Editor, in particular when you have a custom blockRendererFn. Note that I want to force a re-render even though the EditorState has not changed. My custom blockRendererFn depends on state outside the EditorState during its render.

  • I can get a re-render to work by using forceSelection but then I end up with a selection. I don't want the selection, just the re-render. I also don't necessarily want focus.
  • I tried using forceUpdate() on the Editor component but it is not re-rendering. I'm guessing this is because the EditorState has not changed.

Note: I'm asking this question as a workaround to not having a custom shouldComponentUpdate as per my issue here https://github.com/facebook/draft-js/issues/457 and related to https://github.com/facebook/draft-js/issues/267

thesunny avatar Jun 10 '16 21:06 thesunny

Ignore the close/open of the issue above. Hit the wrong button.

I did find a workaround that is ugly as sin but does work.

The editor will re-render if the decorator changes between states. What I did was instead of creating a decorator to style the Editor, I created a function that creates a decorator to style the editor.

The subtle difference is that whenever I call the function, I get a new instance of the decorator even though it does the exact same thing.

Because it is a different instance, it fails the strict equality check in DraftJS's own shouldComponentUpdate and hence re-renders.

I made a function like this in the React component that contains the editor to force the update using the createDecorator method:

  forceRender: function () {
    var editorState = this.state.editorState;
    var content = editorState.getCurrentContent();
    var newEditorState = EditorState.createWithContent(content, createDecorator());
    this.setState({editorState: newEditorState});
  },

It works but I still would prefer a proper solution where we aren't always having to send this forceRender command instead of just checking to see if the contents have changed using a custom shouldComponentUpdate as per this:

https://github.com/facebook/draft-js/issues/457

thesunny avatar Jun 13 '16 21:06 thesunny

Seconded. I'm rendering equations inline using Entities over a single character. Calling Entity.mergeData(...) to update the equation when it's modified in an editor actually updates the editorState the way I would expect it to, but the Entity component isn't updated.

afraser avatar Jun 29 '16 15:06 afraser

+1 on this (and thanks for the hacky workaround), i'm modifying text as the user types and my editorState is showing the replaced text but my UI still isn't, until I force a re-render.

jc4p avatar Aug 28 '16 17:08 jc4p

+1 this is especially important if u do magic things in the blockStyleFN which only get's called on a re-render

maerzhase avatar Sep 19 '16 14:09 maerzhase

@afraser I have similar issue. Have you found any even hacky solution to this ever since you posted here? I do have inline TeX entities and using mergeData the data associated with the entity is indeed updated but the editor is still showing old value.

kevinguard avatar Oct 18 '16 15:10 kevinguard

@kevinguard @maerzhase @jc4p I'm still manually forcing the update of the equation. It's hacky but not terrible since I couple forceUpdateEquation() with the InlineMath component.

Here's the usage in our editor

import InlineMath, { forceUpdateEquation } from 'components/InlineMath'
...
  handleSaveEquation(tex) {
    const { currentEquationEntityKey } = this.state
    if (currentEquationEntityKey) {
      // The user is editing an equation that's already in the editor
      Entity.mergeData(currentEquationEntityKey, {text: tex})
      forceUpdateEquation(currentEquationEntityKey)
    } else {
      // The user is editing a new equation
      this.insertEquation(tex)
    }
    this.hideEquationEditor()
  }
...
  insertEquation(tex) {
    const editorState = this.state.editorState
    const currentContent = editorState.getCurrentContent()
    const entity = Entity.create('equation', 'IMMUTABLE', { text: tex })
    const selection = editorState.getSelection()
    const textWithEntity = Modifier.replaceText(currentContent, selection, " ", null, entity)

    this.setState({
      editorState: EditorState.push(editorState, textWithEntity, "insert-characters")
    })
  }

and here's the inline_math component. I suppose we should be using redux instead of events here, but you get the idea...

import React, { Component, PropTypes } from 'react'
import { Entity } from 'draft-js'
import { trigger, on, off } from '../../utils/events'

//
// Renders an equation entity as InlineTex and binds events neccessary for syncing it
//
export default class InlineMath extends Component {
  static propTypes = {
    entityKey: PropTypes.string.isRequired,
    documentKey: PropTypes.string.isRequired,
    onClick: PropTypes.func,
  }

  getId() {
    return `${this.props.entityKey}`
  }

  componentDidMount() {
    on(`update-equation-${this.props.entityKey}`, () => this.forceUpdate())
  }

  componentWillUnmount() {
    off(`update-equation-${this.props.entityKey}`, () => this.forceUpdate())
  }

  onClick() {
    this.props.onClick(this.props.entityKey)
  }

  render() {
    const { text } = Entity.get(this.props.entityKey).getData()

    return (
      <InlineTex
        id={this.getId()}
        data-document-key={this.props.documentKey}
        onClick={::this.onClick}
        contentEditable={false}
        offsetKey={ this.props.offsetKey }
        tex={text} />
    )
  }
}

//
// Renders tex via Mathjax
//
export class InlineTex extends Component {

  static propTypes = {
    id: PropTypes.string.isRequired,
    tex: PropTypes.string.isRequired,
  }

  render() {
    const { id, tex, offsetKey, contentEditable, ...props } = this.props

    //show the latex symbol as an empty state
    let showableTex = tex ? ("$" + tex + "$") : "$\\LaTeX$"
    return (
      <span className='process_math' data-offset-key={offsetKey} contentEditable={ contentEditable }>
        <span data-text='false' id={id} {...props}>{ showableTex }</span>
      </span>
    )
  }
}

export function forceUpdateEquation(entityKey) {
  trigger(`update-equation-${entityKey}`)
}

afraser avatar Oct 18 '16 19:10 afraser

@afraser Thanks a bunch for sharing the snippet. So essentially you are invoking a method on the component and looks like the method basically doesn't do much but just triggering an event. To me it looks like you're forcing the component to re-render itself, right? Did I understand correctly? Thanks a lot for help.

kevinguard avatar Oct 19 '16 00:10 kevinguard

I have the problem too. Editor.forceUpdate() not trigger the render() of custom atomic block Component.

jan4984 avatar Oct 19 '16 08:10 jan4984

@kevinguard Yes, the event is triggered there and listened to by this line:

on(`update-equation-${this.props.entityKey}`, () => this.forceUpdate())

Note that each unique equation binds to it's own unique event name so forceUpdate() will only be invoked on the equation that needs updating.

afraser avatar Oct 19 '16 12:10 afraser

@jan4984 @kevinguard I'm using MathJax with this config:

    MathJax.Hub.Config({
      extensions: ["tex2jax.js"],
      jax: ["input/TeX", "output/HTML-CSS"],
      tex2jax: {
        ignoreClass: "ignore_math",
        processClass: "process_math",
        inlineMath: [ ['$','$'], ["\\(","\\)"] ],
        displayMath: [ ['$$','$$'], ["\\[","\\]"] ],
        processEscapes: true,
      }
    });

Note the process_math class. That's the same class that's rendered on my InlineMath component to make sure it's automatically rendered by mathjax after it's rendered by React.

afraser avatar Oct 19 '16 12:10 afraser

Thank you very much @afraser. I got it working using React/Redux after learning how you do that using events.

kevinguard avatar Oct 20 '16 06:10 kevinguard

@afraser Just one more question. Have you noticed weird cursor behavior immediately after updating the entity? I am also using Entity.mergeData(...) plus ensuring that when going to mutate the entity content, the editor is set to readOnly mode. Indeed updating the inline TeX works as now I am doing it at the component level but then the cursor jumps around and if I enter something, tons of duplicated characters appear in random places. The workaround is to save the document and then make it editable again which is a terrible user experience. Have you noticed this?

kevinguard avatar Oct 21 '16 00:10 kevinguard

@kevinguard It sounds like the problem you're having is related to the entity being associated with more than one character in the actual document. I'm pretty sure we just decorate a single space (" ") with the equation entity. This will get you most of the way there but there is still another hurdle to jump when an equation happens to fall at the end of a block. The way we handle THAT isn't 100% perfect so I'd rather not share that code just yet.

afraser avatar Oct 21 '16 14:10 afraser

Thanks @afraser. Totally understand that. I actually took a look at the overall code you posted here and just realized that you are using mathjax. I am using KaTeX and that requires rendering as a dangerously formatted string as html. Like you mentioned I am indeed using Modifier.insertTexT() using a single space ' '. I suspect the span is goofing things up when I edit and the entity offset just gets messed up. Still working on it.

kevinguard avatar Oct 22 '16 22:10 kevinguard

I had a similar use case where my decorator needs to use a setting external to the EditorState, that should not be affected by undo/redo. When that setting changed I could not get Draft JS to fully re-render, not by forcing selection, or even after creating a new state and setting a new decorator instance.

My ugly solution was to specify a React key on the Editor component and change it every time my setting changed, forcing react to reinstantiate the editor. Works great, but feels gross.

levilansing avatar Jan 05 '17 22:01 levilansing

One way to force an Editor to re-render that isn't too bad is to force a selection:

const forcedState = EditorState.forceSelection(editorState, editorState.getSelection());

return (
    <Editor
        editorState={someValueChanged ? forcedState : editorState}
    />
);

Apparently even if the selection applied is the same as the existing selection, it treats it as a candidate for re-rendering.

philraj avatar Jan 13 '17 19:01 philraj

@philraj that seems easy, straight-forward and not so bad, but doesn't do it for me. I need it to re-scan and re-apply decorators, which it doesn't do.

gbbr avatar Jan 17 '17 16:01 gbbr

@gbbr Ah too bad. In my case I just needed to re-apply custom styles. I guess the simplest solution in that case is the one by @thesunny. Real shame though.

philraj avatar Jan 17 '17 16:01 philraj

Also had to use @thesunny's hack to get an entity to update too. I'd tried force selection to get it to re-render, but this doesn't do the trick as @gbbr said.

iantanwx avatar Mar 09 '17 05:03 iantanwx

i am new to draftjs .can anyone help of how can we add inline math formula in draftjs

gmchaturvedi1 avatar May 15 '17 18:05 gmchaturvedi1

@gmchaturvedi1 inline custom block only supported if the block only render span

jan4984 avatar May 16 '17 03:05 jan4984

Shall I change div to span in tex example On 16 May 2017 09:07, "JiangYD" [email protected] wrote:

@gmchaturvedi1 https://github.com/gmchaturvedi1 inline custom block only supported if the block only render span

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/facebook/draft-js/issues/458#issuecomment-301666519, or mute the thread https://github.com/notifications/unsubscribe-auth/AKWBKZGm2F1HtDe9jUfy9k_nLo7YiVI2ks5r6RnqgaJpZM4IzWAD .

gmchaturvedi1 avatar May 16 '17 03:05 gmchaturvedi1

I use this to force render everything and still keep my selection state and undo / redo history:

forceRender: function () {
  var editorState = this.state.editorState;
  var contentState = editorState.getCurrentContent();

  var newEditorStateInstance = EditorState.createWithContent(contentState, yourDecoratorStrategy());

  var copyOfEditorState = Draft.EditorState.set(
    newEditorStateInstance,
    {   
      selection: editorState.getSelection(), 
      undoStack: editorState.getUndoStack(), 
      redoStack: editorState.getRedoStack(), 
      lastChangeType: editorState.getLastChangeType() 
    }
  );
  this.setState({editorState: copyOfEditorState});
},

hammadmlk avatar Dec 14 '17 02:12 hammadmlk

One of the workarounds above should do the trick here. Please reopen if there are further questions.

niveditc avatar Sep 08 '18 19:09 niveditc

@thesunny The Workaround you are providing works fine in terms of forcing the editor to re-render but still no the best performant way to achieve and Entity data merge or replace

ipenywis avatar Nov 01 '18 10:11 ipenywis

The workaround is not perfomant on larger content like 500 blocks (paragraphs) also where each word is an entity.

I use draft.js for editing transcripts and each word has data like start-time and duration, hence having an entity for each word.

Laurian avatar Nov 07 '18 22:11 Laurian

Why is this closed?

I wish there was a feature like forceUpdate()

Obiwarn avatar Dec 18 '18 15:12 Obiwarn

Re-opening this issue cause yeah this seems only doable in a hacky way right now. Exposing some forceUpdate also seems non-ideal— it's not idiomatic React to tell your element when to re-render.

I'm thinking one of two things could be done here:

  1. If we see there's any block to be rendered by a custom component, always re-render. This essentially means doing this in the content's shouldComponentUpdate:
    const nextBlocksAsArray = nextProps.editorState
      .getCurrentContent()
      .getBlocksAsArray();
    for (let ii = 0; ii < nextBlocksAsArray.length; ii++) {
      const block = nextBlocksAsArray[ii];
      const customRenderer = nextProps.blockRendererFn(block);
      if (customRenderer) {
        return true;
      }
    }

This means blockRendererFn would be called on every render first to check and then to render. Custom block components would always re-render. We'll still try to be efficient about when we re-render DraftEditorBlocks, but it'll be up to the user to be efficient about their custom blocks.

  1. If we see the entity map changed, always re-render. This essentially means doing this in the content's shouldComponentUpdate:
    const nextEntityMap = nextEditorState.getCurrentContent().getEntityMap();
    const prevEntityMap = prevEditorState.getCurrentContent().getEntityMap();

    if (nextEntityMap !== prevEntityMap) {
      return true;
    }

The entity API is a little awkward right now, but I think it makes sense for all data rendered by Draft.js blocks to go through entities. Custom block components and blockRendererFn wouldn't be called if entities don't change, and this potentially lets draft memoize custom block components for you, so that only the blocks whose data changes attempt a re-render.

Either way this will move re-render optimization away from DraftEditorContents and into individual blocks, which I think is the right move. Choosing to re-render or not at such a high level doesn't really make sense. I'm leaning towards number 2 as the right solution.

mrkev avatar Sep 29 '20 18:09 mrkev

Following up on my previous comment: I'm still leaning mostly towards option 2. It seems to be blocked on the incomplete migration out of DraftEntity that was set to be done by v0.11.0 (oops), so I'll try to get that out by v0.12.0.

Note, this does mean data for custom blocks has to come from entities. I do think this is the right architecture for this. Perhaps the API for blockRendererFn should be changed so it's not a component, but an instance that gets passed into DraftEditor too (kind of how children works), but in any case, I don't think a function to force updates is the move here.

mrkev avatar Oct 01 '20 16:10 mrkev

Bumped into this too, more dynamic custom block styles (e.g. one that depends on caret location) are impossible without more granular rendering.

AndrewPrifer avatar Aug 23 '21 22:08 AndrewPrifer

Following your idea but a little less hacky I feel is:

React.useEffect(() => {
        setEditorState(
            EditorState.forceSelection(editorState, editorState.getSelection())
        )
}, [depsThatForceReRender])

asiraky avatar Feb 10 '22 03:02 asiraky