Are people running into "Calls to EditorView.update are not allowed while an update is in progress" often?

One of the major causes of pain in our application is the error “Calls to EditorView.update are not allowed while an update is in progress”.

I don’t know if we’re misusing CodeMirror but it feels very easy to fall into this trap. What mental models do people follow to make sure they don’t hit this error?

As an example, I hit this today. Take our LSP diagnostics plugin, has the following code

export function createDiagnosticsPlugin(lspClient: LSPClient): Extension {
  return ViewPlugin.define((view) => {
    const dispose = lspClient.onDiagnostics((event) => {
      const diagnostics = lspToCmDiagnostic(event.diagnostics, view.state);
      view.dispatch(setDiagnostics(view.state, diagnostics));
    });

    return {
      destroy() {
        view.dispatch(setDiagnostics(view.state, []));
        dispose();
      },
    };
  });
}

In the example above, the destroy callback leads to the error because there’s an update in progress and so trying to clean up diagnostics doesn’t work.

We have a bunch of setTimeouts wrapping calls to view.dispatch because sometimes we just can’t find a better way. It feels very wrong though and can lead to more errors that are hard to debug down the line.

The approach of an atomic state updated (from the previous state) in explicit, serial steps is incompatible with firing new updates during an update, so this restriction seems hard to avoid.

It is often possible (and preferable, efficiency-wise) to reorganize things in such a way that all the state effects you need originate from a single transaction (in this case, the code that removes the diagnostics plugin would also need to remove the existing diagnostics). Or a transaction extender could be set up to do this.

If all that fails for you, I might be open to adding a utility method on EditorView that take a () => TransactionSpec function, and makes sure the resulting transaction is applied right after the current update, triggering another update cycle. But again, this feels messy and somewhat sub-optimal.

2 Likes

Yeah the motivation for it totally makes sense. Also helps prevent infinite loops, and spending too much time hogging the main thread. I think simply adjusting our mental model of how things work should be enough, but I don’t have an intuition on what is a callback called as part of an update vs what is a callback that is allowing me to update.

I took a closer look at where we mess this up, it’s usually related to two things:

  1. We miss which callbacks are part of the update lifecycle and which are not. For example, tooltip.create CodeMirror Reference Manual is called as part of the tooltip plugins update callback, but you’d have to dig into the source or run into the error to understand that. Or in my example above, I didn’t realize destroy is part of an update.
  2. We yield to our UI library sometimes during the update lifecycle which can lead to more update attempts. I’m trying to come up with a way where we have a batch and flush type interaction between CodeMirror and React to prevent this type of thing.

I think a utility might not be necessary since people can supply their own dispatch function. If we’re considering API tweaks, I would suggest exposing some sort of view.isUpdating boolean (or the currently private view.updateState) to make such an implementation easy in userspace.

Basically everything that happens as part of the view update (as opposed to the state update) is part of the view update cycle, and that includes all plugin lifecycle methods.

You really don’t want to do that. Updating should ideally consist of a preparation stage where all necessary information is collected, and then an update stage which uses a transaction to push all the required updates, as a single transaction, into the editor.

3 Likes