Draft-js: Not being able to update editor state

Created on 18 Apr 2016  路  10Comments  路  Source: facebook/draft-js

I wasn't sure how to describe the issue. I noticed the following behaviour while working on simple Entity.create scenarios.
I have the following

class SimpleEditorComponent extends Component {
  constructor(props) {
    super(props)
    this.state = { editorState: EditorState.createEmpty() }
    this.focus = () => this.refs.editor.focus()
    this.onChange = (editorState) => this.setState({ editorState })
    this.applyEntity = this.applyEntity.bind(this)
    this.logState = () => {
      const content = this.state.editorState.getCurrentContent()
      console.log(convertToRaw(content))
    }
  }
  applyEntity() {
    const { editorState } = this.state
    const entityKey = Entity.create('LINK', 'MUTABLE', { url: 'http://something.com' })
    this.setState({
      editorState: RichUtils.toggleLink(
        editorState,
        editorState.getSelection(),
        entityKey
      ),
    }, () => {
      setTimeout(() => this.refs.editor.focus(), 0)
    })
  }
  render() {
    const { editorState } = this.state
    return (
      <div className="editor" onClick={this.focus}>
        <Editor
          editorState={editorState}
          onChange={this.onChange}
          placeholder="Enter some text..."
          ref="editor"
          />
        <div onClick={this.applyEntity} style={{cursor: 'pointer'}}>Apply entity</div>
        <div onClick={this.logState}>Log editor state</div>
      </div>
    )
  }
}

Which is a very basic setup. However the entity is not added to the editor state. In fact if I set editorState to {} it won't change. Any other properties I set would set on state in applyEntity appear in state on next render. I'm sure it's something obvious but I don't even know where to start.
I thought it might be a race condition because of the editor losing focus when I click Apply entity and then gaining it back. But then nothing would be updated in the state object.

documentation

Most helpful comment

@iansinnott, the editor still loses focus with onMouseDown, that's not the underlying issue. The problem here is the onClick={this.focus} on the wrapping div. It makes this.focus being called when you click on the "Apply Entity" button. These two events are processed one after the other, kind of like this:

1) applyEntity is called and creates the new EditorState with the new entity. It calls setState but it's not set right away because it's an asynchronous operation.
2) Draft internally processes the focus event and updates the EditorState by triggeringonChange. Notice that at this point Draft still doesn't know about the new EditorState from applyEntity.
3) SimpleEditorComponent onChange is called with the new EditorState that calls setState effectively overriding the one set at 1).

The difference that using onMouseDown makes is just the order that these events are fired and processed, onMouseDown is fired and processed before onClick so this gives time for setState called during applyEntity to actually change the state and re-render the component. Then, once, the onClick from the wrapping div is fired and processed Draft already has the EditorState from applyEntity.
For instance, If you move the buttons outside the wrapping div onClick will then work as expected.

I hope this makes sense and clarifies the core issue.

All 10 comments

I think your suggestion of a possible race condition is the likely culprit here. Try using onMouseDown instead of onClick. You might also want to preventDefault() on the event.

@hellendag onMouseDown works, thank you. preventDefault() is not needed.

This probably should be closed. However maybe we should somehow add it to docs. In fact now when I think about it onMouseDown is used in most of the examples so maybe we should add at least a little note about this.

Seems like it might be useful to have a "Tips & Tricks" section in the documentation, to track pointers like these. Sort of like the "Issues & Pitfalls", but more like advice than like "please don't do that". :)

馃憤 for a Tips & Tricks section. I spent a good amount of time figuring this out.

Does anyone know why this is? Why does onMouseDown preserve focus (even without preventing default) while onClick causes the editor to lose focus?

@iansinnott, the editor still loses focus with onMouseDown, that's not the underlying issue. The problem here is the onClick={this.focus} on the wrapping div. It makes this.focus being called when you click on the "Apply Entity" button. These two events are processed one after the other, kind of like this:

1) applyEntity is called and creates the new EditorState with the new entity. It calls setState but it's not set right away because it's an asynchronous operation.
2) Draft internally processes the focus event and updates the EditorState by triggeringonChange. Notice that at this point Draft still doesn't know about the new EditorState from applyEntity.
3) SimpleEditorComponent onChange is called with the new EditorState that calls setState effectively overriding the one set at 1).

The difference that using onMouseDown makes is just the order that these events are fired and processed, onMouseDown is fired and processed before onClick so this gives time for setState called during applyEntity to actually change the state and re-render the component. Then, once, the onClick from the wrapping div is fired and processed Draft already has the EditorState from applyEntity.
For instance, If you move the buttons outside the wrapping div onClick will then work as expected.

I hope this makes sense and clarifies the core issue.

Since we merged the new docs let's close this issue. 鈿★笍馃幐馃幎
Thanks again @aadsm for writing those docs!

Just found this. For anyone who came back here looking for a solution (wherein a button inside the container div doesn't do as expected onClick), another (proper imo) solution is to useevent.stopPropagation in your click handler. This would make sure that your container onClick isn't triggered.

@aadsm Should this be added to the docs?

@an5rag, sorry I missed your comment.

I think it can be solved by not having 2 nested clicks. I don't think that is a good pattern in this situation. I would do:

return (
  <div>
    <div className="editor" onClick={this.focus}>
      <Editor
        editorState={editorState}
        onChange={this.onChange}
        placeholder="Enter some text..."
        ref="editor"
      />
    </div>
    <div onClick={this.applyEntity} style={{cursor: 'pointer'}}>Apply entity</div>
    <div onClick={this.logState}>Log editor state</div>
  </div>
)

@aadsm, that would be ideal. However, I'm talking about the situation where someone might want to place a button _inside_ the editor.

Was this page helpful?
0 / 5 - 0 ratings