One roadblock I find myself running into pretty frequently is making state outside of a ProseMirror document or plugin available to the view. Using EditorView#setProps works for a lot of cases where we need to trigger an update based on something changing outside of the editor, but one thing I can’t figure out a way to influence via props is a widget decoration.
This code sandbox shows a (contrived) example of what I’m talking about. It passes a color theme from outside the editor as a prop, updating it whenever it changes. It uses a plugin view to set CSS color variables of the theme (this works fine) and builds a widget decoration showing which theme was picked (this doesn’t update without an additional transaction being fired).
From what I can tell, I can’t access the view from anywhere other than the WidgetConstructor. This lets me access the up-to-date prop via EditorView#someProp, but I can’t create a key using that value in the widget spec to indicate the data has changed and that the widget constructor function should run again.
To get around this, I dispatch a dummy setMeta transaction most of the time to force a re-render, but this causes an additional update step to be performed. I’m curious to know if anyone has any alternatives, and more generally, a pattern for including state external to the editor in the update cycle.
Can you be more specific on how you imagine external state being passed as editor state?
Here a modified sandbox that implements a redux-like pattern to call EditorView#updateState with an additional property, similar to the data flow section of the ProseMirror guide. I’m passing the state as follows:
Beyond the awkward type augmentation, we can’t spread that additional property because what we’re passing to updateState is an instance of the EditorState class.
There’s a larger issue though: we have the same problem with triggering the apply method of the StateField without triggering also dispatching a transaction. Moving the generation of the DecorationSet to the plugins props.decorations gives us access to the up-to-date state value, but then we don’t have an opportunity for the plugin state to be re-computed.
Is there a way of passing the state as a plugin for better typing? How can plugin state be updated without a transaction?
My suggestion was indeed to use plugin state for this. And no, you cannot update it without a transaction, but having a clear transaction to indicate that the state changed seems a good thing, rather than a downside.
It’s true that if the state can be owned by a plugin, that’s a good place for it. We make use of that pattern elsewhere in our application, using PluginKey#getState to read a plugin-owned value out of the editor state and dispatch commands with transactions that have their meta value set, so that the plugin can update its internal state. This codesandbox provides an example of that.
My question is about when this state doesn’t make sense to be owned by a plugin. Using my example above, what if the state for a theme needed to be shared among two instances of a Prosemirror view, each with their own editor state? Would you duplicate that value as plugin state, keep track of when it changes, and dispatch transactions to keep it in sync with the state of both plugins?
You need to notify all the editors when it changes anyway, it sounds like.
I think that’s why it would be useful to pass a value managed outside of the editor as a prop instead of going through a bunch of machinery to update the plugin state. Is there no way to force the apply method of plugin state to run when calling EditorView#setProp or EditorView#update?
That is true. But decorations can be constructed in plugin state, and a widget’s toDOM function receives the view when rendered. I can access a prop of the view in that toDOM function, but calling setProp doesn’t cause the decoration itself to be re-rendered. This leads to a stale value of whatever prop existed on the view when the WidgetViewDesc was last created, until a transaction is dispatched.
I suppose what’s happening is that your old and recreated widgets use the same render function, and the view assumes that means they don’t need to be re-rendered. Caching the toDOM function by the data it depends on, and making sure you pass a different function when that changes, might help with that.