Closed BrianHung closed 8 months ago
It's not safe to update the state of the view that way. We haven't made any attempt to intercept calls to updateState()
or setProps()
or anything like that. If you want to control the state, you need to pass state
into the <ProseMirror />
component and if you want to update that state you should update it using normal React idioms.
After reading the prosemirror-view
source code, there's not a direct way to listen to state changes made with setProps
or updateState
on the EditorView
; dispatchTransaction
works only for transactions, and creating or deleting plugins doesn't fall into that.
There is perhaps a plugin way
class StateListenerView {
constructor(readonly view: EditorView, readonly listeners: Set<EditorListener>) {
listeners.forEach(l => l(view, undefined));
}
update(view, prevState) {
this.listeners.forEach(l => l(view, prevState));
}
destroy() {
const view = this.view;
this.listeners.forEach(l => l(view, undefined));
}
}
const ReactViewSync = new Plugin({
view: view => new StateListenerView(view, listeners),
});
// setState could be a listener if editorProps.setState existed
function maybeSetState(view, prevState) {
if (view.state !== state) setState(view.state) // view.setProps or view.updateState was called instead of setState
}
but then it's no longer a uni-directional data flow where React is always the source of truth.
This would be the React idiomatic way instead.
export const EditorWordCount = ({ state, setState }) => {
useEffect(function initPlugin() {
const plugin = WordCount()
setState(state =>
state.reconfigure({
plugins: state.plugins.concat([plugin]),
})
);
return () => {
setState(state =>
state.reconfigure({
plugins: state.plugins.filter(p => p !== plugin),
})
);
}
}, []);
const count = state && WordCountKey.getState(state);
if (!count) return;
return (
<div className="space-x-2 px-4 py-2 text-right text-xs tabular-nums text-gray-400">
<span>Characters: {count.characters}</span>
<span>Words: {count.words}</span>
<span>Sentences: {count.sentences}</span>
</div>
);
};
That's a very cool example! I haven't seen any dynamic registration of state plugins this way. We have similar components at @nytimes that read from plugin state, but we generally just configure the plugin when we create the editor state. It's neat the way you've made this self-contained.
It might make sense to make some kind of API in react-prosemirror for registering stateful plugins like this. If you find yourself repeating this pattern a lot, hop into the Gitter room and talk with us about maybe putting something into the library to support it, please!
I have a React component that on-mount, creates a ProseMirror plugin and reconfigures the view state to include it. (This paradigm of React component registering plugins themselves makes sense when editor ui is conditional but still complicated enough to need access to state and view plugins).
The issue is that
useEditorState
doesn't update after theview.updateState
call.I checked the source code of this library and there doesn't seem to be any code that calls
setState
afterview.updateState
orview.setProps({ state })
is called.WordCount Plugin source code
```ts import { Plugin, PluginKey } from 'prosemirror-state'; export const WordCountKey = new PluginKey('WordCount'); export function WordCount() { /** * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter */ const grapheme = new Intl.Segmenter(undefined, { granularity: 'grapheme' }); const word = new Intl.Segmenter(undefined, { granularity: 'word' }); const sentence = new Intl.Segmenter(undefined, { granularity: 'sentence' }); const segment = (text: string) => ({ characters: [...grapheme.segment(text)].length, words: [...word.segment(text)].filter(segment => segment.isWordLike).length, sentences: [...sentence.segment(text)].length, }); return new Plugin({ key: WordCountKey, state: { init(config, state) { let textContent = state.doc.textBetween(0, state.doc.content.size, ' ', ' '); return segment(textContent); }, apply(tr, pluginState, prevState, state) { if (!tr.docChanged) return pluginState; let textContent = state.doc.textBetween(0, state.doc.content.size, ' ', ' '); return segment(textContent); }, }, }); } export default WordCount; ```