Search code examples
javascriptreactjsreact-hooksdraftjs

Draft JS editor does not update it's content when its value changed by parent component?


I've a simple use case: There's a DraftEditor component which takes value as its prop and create a editor state based on value (empty or with content). It possible that the value gets changed by the parent and when it does, I expect the Draft Editor to update it's content as well. Here's my DraftEditor component.

import React, { useState } from "react";
import { Editor, EditorState, convertFromRaw } from "draft-js";

export default ({ value }) => {
  const initialState = value
    ? EditorState.createWithContent(convertFromRaw(JSON.parse(value)))
    : EditorState.createEmpty();
  const [editorState, setEditorState] = useState(initialState);

  return <Editor editorState={editorState} onChange={setEditorState} />;
};

The problem: When value is updated by the parent component, the contents of Editor is not getting updated. Instead, it just shows the content it was initialized with. The workaround I found is to manually call setEditorState when the value changes, but I feel this step is unnecessary as when the component re-renders I expect the editor to recalculate it's internal state as well? May be I am missing something here?

Any idea why the Editor is not updating it's internal state?

Here's a code sandbox: https://codesandbox.io/s/xenodochial-sanderson-i95vd?fontsize=14&hidenavigation=1&theme=dark


Solution

  • The basic problem is

    const [editorState, setEditorState] = useState(initialState);
    

    uses it's parameter initialState only once (on initial run-through), no matter how many times initialState changes.

    When using useState() and there is a prop (or other) dependency, pair it with a useEffect() to make things reactive.

    This may seem a bit backwards, but a lot of (most of) the hooks are about keeping things the same when the Function component is re-run. So useState() only updates editorState via setEditorState, after the initial call.

    import React, { useState, useEffect } from "react";
    import { Editor, EditorState, convertFromRaw } from "draft-js";
    
    export default ({ value }) => {
    
      const [editorState, setEditorState] = useState();
    
      useEffect(() => {
        const state = value
          ? EditorState.createWithContent(convertFromRaw(JSON.parse(value)))
          : EditorState.createEmpty();
        setEditorState(state);
      }, [value]); // add 'value' to the dependency list to recalculate state when value changes.
    
      return <Editor editorState={editorState} onChange={setEditorState} />;
    };
    

    In the code above, editorState will be null on initial component call. If this is a problem for the Editor component, you can externalize the state calculation in a function and call it for useState() and within useEffect().

    import React, { useState, useEffect } from "react";
    import { Editor, EditorState, convertFromRaw } from "draft-js";
    
    export default ({ value }) => {
    
      const [editorState, setEditorState] = useState(calcState(value));
    
      useEffect(() => {
        setEditorState(calcState(value));
      }, [value]); // add 'value' to the dependency list to recalculate state when value changes.
    
      return <Editor editorState={editorState} onChange={setEditorState} />;
    };
    
    const calcState = (value) => {
      return value
        ? EditorState.createWithContent(convertFromRaw(JSON.parse(value)))
        : EditorState.createEmpty();
    }
    

    Since those two hooks are used together a lot, I tend to pair them in a custom hook to encapsulate the detail.

    The difference is that the custom hook runs every time the Component runs, but editorState still only updates when value changes.

    Custom hook

    import React, { useState, useEffect } from "react";
    import { EditorState, convertFromRaw } from "draft-js";
    
    export const useConvertEditorState = (value) => {
    
      const [editorState, setEditorState] = useState(calcState(value));
    
      useEffect(() => {
        setEditorState(calcState(value));
      }, [value]); 
    
      return [editorState, setEditorState];
    }
    
    const calcState = (value) => {
      return value
        ? EditorState.createWithContent(convertFromRaw(JSON.parse(value)))
        : EditorState.createEmpty();
    }
    

    Component

    import React, { useState, useEffect } from "react";
    import { Editor } from "draft-js";
    import { useConvertEditorState } from './useConvertEditorState'
    
    export default ({ value }) => {
    
      const [editorState, setEditorState] = useConvertEditorState(value);
    
      return <Editor editorState={editorState} onChange={setEditorState} />;
    };