CKEditor custom builds in React

Recently I've been working on a project that uses a CKEditor custom build. While CKEditor is an impressive piece of software, React is extremely picky about how you import the library.

I did a lot of trial and error to get it working on my project, so I thought a write-up might be useful

You have to import three components: the base library, the template for how you want the editor to look - buttons, fonts, text color etc - and the CSS. Importing the base component is easy enough, but the template is... let's just call it "non standard."

Below is how it looks in practice. There's the default ckeditor5 package in the app, and then a custom package that is kept packages subdirectory with its own package.json and build process. I use Node's Workspace feature to keep them in the same repository. Note that the last import is not named.

import { CKEditor } from '@ckeditor/ckeditor5-react'
import '../../packages/ckeditor5/styles.css'
import 'ckeditor5-custom-build/build/ckeditor'

Later on in the file, when it's time to render the component, it looks like this.

<CKEditor
      id={id}
      data = {data}
      editor={ClassicEditor}
      onChange={(event, editor) => {
        onChange(String(id), editor.getData());
      }}
      onReady={ editor => {
            if (data) editor.setData(data)
        }}
      // I have no idea why this works. Lots of conflicting advice on stackoverflow
      // https://stackoverflow.com/questions/74559310/uncaught-syntaxerror-the-requested-module-ckeditor5-build-ckeditor-js-does-n 

      config={{
                simpleUpload: simpleUploadConfig(), 
                image: {upload: {types: [ 'png', 'jpeg','gif' ]}}
              }}    
    />

This is an exact copy and paste from my file, complete with the befuddled comment referencing StackOverlow.

The most puzzling part is here:

editor={ClassicEditor}

There is no variable named ClassicEditor previously defined in the file. It just... works? You may think that's because the unnamed import up top has a default export:

import 'ckeditor5-custom-build/build/ckeditor'

...but it's not. I suspect CKEditor is defining some kind of global variable and injecting into the namespace, but I haven't taken a deep enough dive into the source to figure out why.

"Whatever, it works, right?"

EXCEPT when testing! For some reason Vitest and Jest are not able to see that ClassicEditor variable at runtime, and everything grinds to a halt.

So herewith my workaround. We define a mock ClassicEditor with the two necessary methods for rendering. Then we change it depending on the environment.

let ClassicEditor: any = {
  getData: () => {return 'test'},
  setData: (data: string) => {return data}
}
if(import.meta.env.VITE_APP_ENV !='testing'){
  import('ckeditor5-custom-build/build/ckeditor').then(module => {
    ClassicEditor = module.default;
  });
}

In the test environment, ClassicEditor provides just enough functionality for the page to render. In dev and production, we redefine the variable after a module import.

I hope you find this useful!