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
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} />;
};