RangeError: Adding different instances of a keyed plugin (plugin$)

I’m loading plugins dynamically using ES6 dynamic imports. My API entry is through tiptap v2. What I’m now noticing that whenever I add the extension-gapcursor (which effectively is a thin layer over PMs gapcursor module) to be dynamically imported - I get an error.

This is the error:

Uncaught (in promise) RangeError: Adding different instances of a keyed plugin (plugin$)
    Configuration2 state.js:50
    Configuration2 state.js:48
    reconfigure state.js:204
    createView Editor.ts:273
    Editor Editor.ts:85
    <anonymous> main.ts:47
    promise callback* main.ts:27
    promise callback* main.ts:18

As a n00b this to me seems like some synthesized key somewhere (plugin$) that is somehow generated twice (is this like a pluginKey?). I tracked the origin of the plugins to:

  • prosemirror-keymap and
  • prosemirror-gapcursor

I do not have a clue what I can do about this. Could this be a issue in/with PM?

The key plugin$ is created inside of PM - so is there any reason to think that somehow duplicate keys are being generated from within PM?

You’re somehow adding the same plugin (or two plugins with the same explicitly given key value) twice in your configuration. Shouldn’t be too hard to diagnose.

That’s the thing… I don’t :roll_eyes:

The duplicate keys are coming from: keymap and gapcursor at least that is what a debug session is revealing to me. These don’t have explicit keys.

Could this be a result of dynamic imports and promise resolution?


When I change one of the plugin’s, synthesized keys (plugin$) in-flight as part of a debug session to something like plugin$aap everything runs fine.

Is it possible that you’re loading two instances of prosemirror-state? That might cause this (and will break other things as well).

Okay - sorry this might sound like a stupid question, but given that I don’t knowingly load anything that I don’t specify how can I find out if prosemirror-state is loaded twice and what it is that is loading it twice?

npm ls prosemirror-state will show you whether you have multiple instances installed. But even if there’s only one there, whatever loading strategy you’re using might still end up loading it twice. You could add console.log statements to the top of node_modules/prosemirror-state/dist/*.js and see how many of them come by in your browser console.

Thank you for all your insights and help, in the mean time I think I spotted some suspicious looking code in tiptap v2, which I think introduces the duplicatie plugin$. I don’t have a clue how to prevent this from happening and/but I’ll direct my question to them.

For the record the code is in tiptap’s ExtensionManager on a getter of Plugin[] ~ which actually augments the plugins based on the property for addKeyboardShortcuts

        if (addKeyboardShortcuts) {
          const bindings = Object.fromEntries(
              .map(([shortcut, method]) => {
                return [shortcut, () => method({ editor })]

          const keyMapPlugin = keymap(bindings)


It is in that piece of the code that plugins starts storing duplicate keys.

@marijn - any reason why gapCursor does not specify a plugin key?

The actual duplicate plugins are due to:

  - shortcut key (Mod-Alt-0) plugin, stored as plugin$
  - addProseMirrorPlugins, stored as plugin$

This seems beyond my control.

Hey @pojo, don’t confuse the ProseMirror forum with all the Tiptap support channels (GitHub issues, GitHub discussions, Discord, Email). I feel bad if Tiptap users ask Marijn for help. On the one hand it’s hard for him to help with packages he doesn’t even know, and on the other hand it’s unlikely you’ll find solutions here. Please open an issue in the Tiptap repository and we’ll make sure to look into it:

Sure @hanspagel

Maybe you saw that my last question was actually a PM specific one?

In case someone is interested in the outcome of this specific issue. For my dev environment I managed to resolve it through configuring vitejs appropriately (I think).

  1. install prosemirror-model, prosemirror-state, prosemirror-view and prosemirror-transform
  2. make the following change in vite.config.js
optimizeDeps: {
    include: [

The result of this are 4 separate chunks that will be loaded, which as a result makes the problem go away.