EditorState Race Conditions

Draft Editor is a controlled input component (you can read about this in detail in the API Basics section), meaning that changes made to the Editor state are propagated upwards through onChange and it's up to the app to feed it back to the Editor component.

This cycle usually looks like:

...
this.onChange = function(editorState) {
this.setState({editorState: editorState});
}
...
<Editor
editorState={this.state.editorState}
onChange={this.onChange}
placeholder="Enter some text..."
/>

Different browser events can trigger the Editor to create a new state and call onChange. For instance, when the user pastes text into it, Draft parses the new content and creates the necessary data structure to represent it.

This cycle works great, however, it is an asynchronous operation because of the setState call. This introduces a delay between setting the state and rendering the Editor with the new state. During this time period other JS code can be executed.

Race condition diagram 1

Non-atomic operations like this can potentially introduce race conditions. Here's an example: Suppose you want to remove all the text styles that come from the paste. This can be implemented by listening to the onPaste event and removing all styles from the EditorState:

this.onPaste = function() {
this.setState({
editorState: removeEditorStyles(this.state.editorState),
});
};

However, this won't work as expected. You now have two event handlers that set a new EditorState in the exact same browser event. Since the event handlers will run one after the other only the last setState will prevail. Here's how it looks like in the JS timeline:

Race condition diagram 2

As you can see, since setState is an asynchronous operation, the second setState will override whatever it was set on the first one making the Editor lose all the contents from the pasted text.

You can observe and explore the race condition in this running example. The example also has logging to highlight the JS timeline so make sure to open the developer tools.

As a rule of thumb avoid having different event handlers for the same event that manipulate the EditorState. Using setTimeout to run setState might also land you in the same situation. Anytime you feel you're “losing state” make sure you're not overriding it before the Editor re-rendering.

Best Practices

Now that you understand the problem, what can you do to avoid it? In general be mindful of where you're getting the EditorState from. If you're using a local one (stored in this.state) then there's the potential for it to not be up to date. To minimize this problem Draft offers the latest EditorState instance in most of its callback functions. In your code you should use the provided EditorState instead of your local one to make sure you're basing your changes on the latest one. Here's a list of supported callbacks on the Editor:

  • handleReturn(event, editorState)
  • handleKeyCommand(command, editorState)
  • handleBeforeInput(chars, editorState)
  • handlePastedText(text, html, editorState)

The paste example can then be re-written in a race condition free way by using these methods:

this.handlePastedText = (text, styles, editorState) => {
this.setState({
editorState: removeEditorStyles(text, editorState),
});
};
//...
<Editor
editorState={this.state.editorState}
onChange={this.onChange}
handlePastedText={this.handlePastedText}
placeholder="Enter some text..."
/>;

With handlePastedText you can implement the paste behavior by yourself.

NOTE: If you need to have this behavior in your Editor, you can achieve it by setting the Editor's stripPastedStyles property to true.

Last updated on by Yangshun Tay