React controlled editor
There are times when you will want complete control over the content in your editor. For this reason remirror supports controlled editors. Setting up your editor like this is more complicated due to the asynchronous nature of react updates versus the synchronous nature of ProseMirror dispatch
. It's easy to get yourself in trouble without taking care to understand the concepts. If in doubt, start with an uncontrolled editor and upgrade to controlled once you're more comfortable with remirror
.
Advanced Topic
The following is considered an advanced topic. If you are struggling to understand some of the concepts don't feel bad. It can be hard to understand initially.
Get started in the usual way.
The main difference is that you will need to create the state value that is passed into the editor. This value is called the EditorState
and is an object that will be familiar to you if you have used ProseMirror
in the past. When remirror sees the value it knows to treat the editor as a controlled instance. For things to work correctly you are required to add an onChange
handler for the RemirrorProvider
.
The editor now behaves in a similar way to what you'd expect from a non controlled editor. The main thing is that we've been able to intercept the state update and can do some pretty interesting things with this power.
For example, the following change handler now intercepts the state update in order to insert NO!!!
into the editor whenever the user types any content.
Potential pitfalls
Commands use the current state stored on view.state
to dispatch transactions and create a new state. In an uncontrolled editor this is perfectly fine since the source of truth is view.state
. Once an update is dispatched the state is updated synchronously and onChange
is immediately called. With controlled editors there's often a delay between the view.dispatch
and the state being updated.
When attempting to synchronously run multiple commands in a controlled editor, each command operates on the current state, not the state as applied by the previous command. As a result, we find ourselves in a situation where the last command wins. i.e. the last state update before the controlled editor can apply the new state is the one that will be used.
Since the playground is a controlled editor, you can observe the phenomenom there.
I created a controlled editor test showing that this is actually expected behaviour. Making multiple state updates before the state has been updated will not work in a controlled editor.
Chained Commands
The advised workaround is to use chained
commands.
Chained commands allow composing different commands together that have been updated to work with the ProseMirror transaction
rather than the fixed state. This means that each command adds new steps and when the .run()
is called all these steps are dispatched at the same time.
However, not all commands are chainable.
There are some that will never be chainable.
undo
redo
These only work with synchronous state updates and it doesn't really make sense to use them as part of a chain. Calling them with chained
will throw an error and if you're using TypeScript your code will complain at compile time.
There are some that still need to be made chainable
In #422 most commands have been made chainable. @remirror/preset-table
and @remirror/preset-list
have been left out for now since they require a bit more work to convert their commands to rely on transactions rather than state.
When I have time, I'll need to convert the commands that are currently being imported by prosemirror-tables
and prosemirror-schema-list
to use transactions instead of state. At this point it might even make sense to remove these libraries from @remirror/pm
.
You can see an example of one such conversion here.
Workarounds
- Use
chained
commands where possible. - Commands that update the transaction will also work since the same transaction is shared, however it's advisable to be explicit about the intent to chain commands together.
- Certain commands will never be chainable. If you are using TypeScript this will be obvious as they are non-callable.
- Work will be done to convert the
@remirror/preset-tables
library and@remirror/preset-list
to use chainable commands.