Draft.js: Callback to a class component results in new object in parent instead of modified

Viewed 167

I am using Draft.js to create Rich Text Editor.

This is Parent:

    import React from 'react'
    import RichTextEditor from './RichTextEditor'
    import {useEffect, useState, useCallback} from "react"
    import { EditorState, convertToRaw } from 'draft-js';
    function AddPage() {

      // MY STATE
      const [editorState, setEditorState] = useState({editorState: EditorState.createEmpty()});
    
      // MY CALLBACKS ATTEMPTS
      function onChangeCallback(editorState) {
        const contentState = editorState.getCurrentContent();
        console.log(convertToRaw(contentState))
        setEditorState({editorState});
      }
      // const onChangeCallback = useCallback((editorState) => {
      //   const contentState = editorState.getCurrentContent();
      //   console.log(convertToRaw(contentState))
      //   setEditorState({editorState});
      // }, []); 
      // const onChangeCallback = ({editorState}) => { 
      //   setEditorState({editorState});
      // }
    
      return (
        <div>
    
          <div className="app__body">
            {editorState && <RichTextEditor editorState={editorState} onChangeCallback={onChangeCallback} /> }
        </div>
    
        </div>
      )
    }
    
    export default AddPage

And this is Child (I made it as short as I could)

    import React from 'react'
    import { Editor, RichUtils, getDefaultKeyBinding } from 'draft-js';
    import './RichTextEditor.css'
    import 'draft-js/dist/Draft.css';
    
    
    class RichTextEditor extends React.Component {
      constructor(props) {
        super(props);
        this.state =  this.props.editorState; // <----- PASSED STATE
    
        this.onChange = this.props.onChangeCallback; //<----- PASSED CALLBACK
    
        this.focus = () => this.refs.editor.focus();
    
        this.handleKeyCommand = this._handleKeyCommand.bind(this);
        this.mapKeyToEditorCommand = this._mapKeyToEditorCommand.bind(this);
        this.toggleBlockType = this._toggleBlockType.bind(this);
        this.toggleInlineStyle = this._toggleInlineStyle.bind(this);
      }
    
      _handleKeyCommand(command, editorState) {
        const newState = RichUtils.handleKeyCommand(editorState, command);
        if (newState) {
          this.onChange(newState);
          return true;
        }
        return false;
      }
    
      _mapKeyToEditorCommand(e) {
        if (e.keyCode === 9 /* TAB */) {
          const newEditorState = RichUtils.onTab(
            e,
            this.state.editorState,
            4, /* maxDepth */
          );
          if (newEditorState !== this.state.editorState) {
            this.onChange(newEditorState);
          }
          return;
        }
        return getDefaultKeyBinding(e);
      }
    
      _toggleBlockType(blockType) {
        this.onChange(
          RichUtils.toggleBlockType(
            this.state.editorState,
            blockType
          )
        );
      }
    
      _toggleInlineStyle(inlineStyle) {
        this.onChange(
          RichUtils.toggleInlineStyle(
            this.state.editorState,
            inlineStyle
          )
        );
      }
    
      render() {
    
        let className = 'RichEditor-editor';
        var contentState = this.state.editorState.getCurrentContent();
        if (!contentState.hasText()) {
          if (contentState.getBlockMap().first().getType() !== 'unstyled') {
            className += ' RichEditor-hidePlaceholder';
          }
        }
    
        return (
          <div className="RichEditor-root">
                  <button onClick={this.onChange}>aaa</button>
    
            <div className={className} onClick={this.focus}>
              <Editor
                editorState={this.state.editorState}
                handleKeyCommand={this.handleKeyCommand}
                keyBindingFn={this.mapKeyToEditorCommand}
                onChange={this.onChange}
                placeholder="Add your text here"
                ref="editor"
                spellCheck={true}
              />
            </div>
          </div>
        );
      }
    }
    
    export default RichTextEditor

My problem is that when I click a key, editorState in Parent callback contains the letter, but when I click again, it looks like editorState is a new object with one, the last clicked letter. I never see anything in the editor.

editorState is the object that should contain a lot of blocks, each corresponding to another line of text. Instead, this is console output:

The result

BTW. I need to store editorState in Parent. When I move more or less the same useState code to the child, it behaves the way it should.

1 Answers

It seems that it was a beginner question hidden in Draft.js. I did not understand basic hooks' functionality. Each time I run callback, setEditorState re-renders the component. To re-render it with a memoized version of the callback, and it’ll only be changed if one of the dependencies has changed in callback (see last line):

  const onChangeCallback = useCallback((editorState) => {
    const contentState = editorState.getCurrentContent();
    console.log(convertToRaw(contentState))
    setEditorState({editorState});
  }, [editorState]); 

This helped me

How to prevent re-rendering of components that have not changed?

Related