Contribute to this guide

Migrating CKEditor 5 React integration from npm to CDN

This guide will help you migrate CKEditor 5 React integration from an NPM-based installation to a CDN-based one.

# Prerequisites

Remove the existing CKEditor 5 packages from your project. If you are using the NPM-based installation, you can remove it by running the following command:

npm uninstall ckeditor5 ckeditor5-premium-features

Upgrade the CKEditor 5 React integration to the latest version. You can find the latest version in the React integration documentation.

Ensure that your testing suite uses real web browser environments for testing. If you are using jsdom or any other environment without a real DOM, you may need to adjust the testing suite configuration to use a real browser because CDN script injection might not be recognized properly in such environments.

# Migration steps

# Migrate CKEditor component

# Step 1: Remove CKEditor 5 imports

If you have any CKEditor 5 imports in your test files, remove them. For example, remove lines like:

import { ClassicEditor, /* ... other imports */ } from 'ckeditor5';
import { AIAdapter, /* ... other imports */ } from 'ckeditor5-premium-features';

# Step 2: Use useCKEditorCloud hook to load CKEditor 5 from CDN

The useCKEditorCloud function is a hook that allows you to load CKEditor 5 from the CDN. It returns an object with the CKEditor and CKEditorPremiumFeatures properties. Here is an example of migrating the basic CKEditor 5 React component:

Before:

import { CKEditor } from '@ckeditor/ckeditor5-react';
import { ClassicEditor, Bold, Essentials, Italic, Mention, Paragraph, Undo } from 'ckeditor5';
import { SlashCommand } from 'ckeditor5-premium-features';

import 'ckeditor5/ckeditor5.css';
import 'ckeditor5-premium-features/ckeditor5-premium-features.css';

function App() {
    return (
        <CKEditor
            editor={ ClassicEditor }
            config={ {
                licenseKey: '<YOUR_LICENSE_KEY>', // Or 'GPL'.
                toolbar: {
                    items: [ 'undo', 'redo', '|', 'bold', 'italic' ],
                },
                plugins: [
                    Bold, Essentials, Italic, Mention, Paragraph, SlashCommand, Undo
                ],
                mention: {
                    // Mention configuration
                },
                initialData: '<p>Hello from CKEditor 5 in React!</p>',
            } }
        />
    );
}

export default App;

After:

import { CKEditor, useCKEditorCloud } from '@ckeditor/ckeditor5-react';

function App() {
    // Load CKEditor 5 from the CKEditor Cloud, using the `useCKEditorCloud` hook.
    // It'll inject the CKEditor 5 scripts and styles into your document head and after
    // successful loading, it'll return the `CKEditor` and `CKEditorPremiumFeatures` objects.
    const cloud = useCKEditorCloud( {
        version: '44.1.0', // Required. The version of CKEditor 5 to load.
        premium: true // Optional. Set to `true` if you want to use premium features.
    } );

    if ( cloud.status === 'loading' ) {
        return <div>Loading...</div>;
    }

    if ( cloud.status === 'error' ) {
        console.error( cloud.error );

        return <div>Error!</div>;
    }

    // Pick the CKEditor 5 plugins you want to use.
    const {
        ClassicEditor, Bold, Essentials, Italic,
        Mention, Paragraph, SlashCommand, Undo
    } = cloud.CKEditor;

    const { SlashCommand } = cloud.CKEditorPremiumFeatures;

    return (
        <CKEditor
            editor={ ClassicEditor }
            config={ {
                toolbar: {
                    items: [ 'undo', 'redo', '|', 'bold', 'italic' ],
                },
                plugins: [
                    Bold,
                    Essentials,
                    Italic,
                    Mention,
                    Paragraph,
                    SlashCommand,
                    Undo
                ],
                licenseKey: '<YOUR_LICENSE_KEY>',
                initialData: '<p>Hello from CKEditor 5 in React!</p>',
            } }
        />
    );
}

# Step 3 (Optional): Migrate the CKEditor 5 React testing suite

If you have any tests that use CKEditor 5 objects, you need to update them to use the loadCKEditorCloud function. Here is an example of migrating a test that uses the ClassicEditor object:

Before:

import { ClassicEditor, /* ... other imports */ } from 'ckeditor5';
import { AIAdapter, /* ... other imports */ } from 'ckeditor5-premium-features';

it( 'ClassicEditor test', () => {
    render(
        <CKEditor
            editor={ ClassicEditor }
            // ... other props
        />
    );
} );

After:

// It may be counterintuitive that in tests you need to use `loadCKEditorCloud` instead of `useCKEditorCloud`.
// The reason for this is that `useCKEditorCloud` is a React hook and can only be used in React components,
// while tests are typically written as functions in testing suites. Therefore, in tests, you should use
// the `loadCKEditorCloud` function to load CKEditor 5 from the CKEditor Cloud and obtain the necessary
// CKEditor 5 objects. This allows you to properly test your CKEditor 5 integration without any issues.

import { loadCKEditorCloud } from '@ckeditor/ckeditor5-react';

let cloud;

beforeEach( async () => {
    cloud = await loadCKEditorCloud( {
        version: '44.1.0',
    } );
} );

it( 'ClassicEditor test', () => {
    const { ClassicEditor } = cloud.CKEditor;

    render(
        <CKEditor
            editor={ ClassicEditor }
            // ... other props
        />
    );

    // Rest of your test.
} );

# Step 4 (Optional): Clean up the document head entries before each test

The useCKEditorCloud hook under the hood injects the CKEditor 5 scripts and styles into your document head. If you use a testing suite that does not Clean up the document head entries before each test, you may need to do it manually. This is important because the useCKEditorCloud hook might reuse the same head entries for each test, which can lead to skipping the loading state and directly going to the success state. It may cause some tests that rely on the loading state to fail.

However, there is one downside to this approach. Cleaning up the head entries before each test may slow down the test execution because the browser needs to download the CKEditor 5 script each time. In most cases, this should not be a problem, but if you notice that your tests are running slower, you may need to consider other solutions.

Here is an example of how you can Clean up the document head entries before each test:

import { removeAllCkCdnResources } from '@ckeditor/ckeditor5-integrations-common/test-utils';

beforeEach( () => {
    removeAllCkCdnResources();
} );

The code above will remove all CKEditor 5 CDN scripts, style sheets, and Window objects from the head section of your HTML file before each test, making sure that the useCKEditorCloud hook will inject the CKEditor 5 scripts and styles again.

# Migrate CKEditorContext component

# Step 1: Remove CKEditor 5 imports

If you have any CKEditor 5 imports in your test files, remove them. For example, remove lines like:

import { ClassicEditor, /* ... other imports */ } from 'ckeditor5';
import { AIAdapter, /* ... other imports */ } from 'ckeditor5-premium-features';

# Step 2: Use useCKEditorCloud hook to load CKEditor 5 Context from CDN

If you use the CKEditorContext component, you need to update it to use the useCKEditorCloud hook. Here is an example of migrating the CKEditorContext component:

Before:

import { ClassicEditor, Context, Bold, Essentials, Italic, Paragraph, ContextWatchdog } from 'ckeditor5';
import { CKEditor, CKEditorContext } from '@ckeditor/ckeditor5-react';

import 'ckeditor5/ckeditor5.css';

function App() {
  return (
    <CKEditorContext context={ Context } contextWatchdog={ ContextWatchdog }>
      <CKEditor
        editor={ ClassicEditor }
        config={ {
            licenseKey: '<YOUR_LICENSE_KEY>', // Or 'GPL'.
            plugins: [ Essentials, Bold, Italic, Paragraph ],
            toolbar: [ 'undo', 'redo', '|', 'bold', 'italic' ],
        } }
        data='<p>Hello from the first editor working with the context!</p>'
        onReady={ ( editor ) => {
            // You can store the "editor" and use when it is needed.
            console.log( 'Editor 1 is ready to use!', editor );
        } }
      />

      <CKEditor
        editor={ ClassicEditor }
        config={ {
            licenseKey: '<YOUR_LICENSE_KEY>', // Or 'GPL'.
            plugins: [ Essentials, Bold, Italic, Paragraph ],
            toolbar: [ 'undo', 'redo', '|', 'bold', 'italic' ],
        } }
        data='<p>Hello from the second editor working with the context!</p>'
        onReady={ ( editor ) => {
            // You can store the "editor" and use when it is needed.
            console.log( 'Editor 2 is ready to use!', editor );
        } }
      />
    </CKEditorContext>
  );
}

export default App;

After:

import { CKEditor, CKEditorContext, useCKEditorCloud } from '@ckeditor/ckeditor5-react';

function App() {
    // Load CKEditor 5 from the CKEditor Cloud, using the `useCKEditorCloud` hook.
    // It'll inject the CKEditor 5 scripts and styles into your document head and after
    // successful loading, it'll return the `CKEditor` and `CKEditorPremiumFeatures` objects.
    const cloud = useCKEditorCloud( {
        version: '44.1.0', // Required. The version of CKEditor 5 to load.
        premium: true // Optional. Set to `true` if you want to use premium features.
    } );

    if ( cloud.status === 'loading' ) {
        return <div>Loading...</div>;
    }

    if ( cloud.status === 'error' ) {
        console.error( cloud.error );

        return <div>Error!</div>;
    }

    // Pick the CKEditor 5 plugins you want to use.
    const {
        ClassicEditor, Bold, Essentials, Italic, Paragraph,
        Context, ContextWatchdog
    } = cloud.CKEditor;

    return (
        <CKEditorContext
            context={ Context }
            contextWatchdog={ ContextWatchdog }
        >
            <CKEditor
                editor={ ClassicEditor }
                config={ {
                    licenseKey: '<YOUR_LICENSE_KEY>',
                    plugins: [ Essentials, Bold, Italic, Paragraph ],
                    toolbar: [ 'undo', 'redo', '|', 'bold', 'italic' ],
                } }
                data='<p>Hello from the first editor working with the context!</p>'
                onReady={ ( editor ) => {
                    // You can store the "editor" and use when it is needed.
                    console.log( 'Editor 1 is ready to use!', editor );
                } }
            />

            <CKEditor
                editor={ ClassicEditor }
                config={ {
                    licenseKey: '<YOUR_LICENSE_KEY>',
                    plugins: [ Essentials, Bold, Italic, Paragraph ],
                    toolbar: [ 'undo', 'redo', '|', 'bold', 'italic' ],
                } }
                data='<p>Hello from the second editor working with the context!</p>'
                onReady={ ( editor ) => {
                    // You can store the "editor" and use when it is needed.
                    console.log( 'Editor 2 is ready to use!', editor );
                } }
            />
        </CKEditorContext>
    );
}

# Next steps

Now that you have migrated your CKEditor 5 React Context integration to use the CDN, you can continue with the next steps, such as the migration testing suite. It is identical to the steps described in the previous section.

# Migrate useMultiRootEditor hook

# Step 1: Remove CKEditor 5 imports

If you have any CKEditor 5 imports in your test files, remove them. For example, remove lines like:

import { ClassicEditor, /* ... other imports */ } from 'ckeditor5';
import { AIAdapter, /* ... other imports */ } from 'ckeditor5-premium-features';

# Step 2: Use withCKEditorCloud HOC to load CKEditor 5 from CDN

If you use the useMultiRootEditor hook, you need to update it to use the withCKEditorCloud HOC. Here is an example of migrating the useMultiRootEditor hook:

Before:

import { MultiRootEditor, Bold, Essentials, Italic, Paragraph } from 'ckeditor5';
import { useMultiRootEditor } from '@ckeditor/ckeditor5-react';

import 'ckeditor5/ckeditor5.css';

const App = () => {
    const editorProps = {
        editor: MultiRootEditor,
        data: {
            intro: '<h1>React multi-root editor</h1>',
            content: '<p>Hello from CKEditor&nbsp;5 multi-root!</p>'
        },
        config: {
            plugins: [ Essentials, Bold, Italic, Paragraph ],
            toolbar: {
                items: [ 'undo', 'redo', '|', 'bold', 'italic' ]
            },
        }
    };

    const {
        editor,
        toolbarElement,
        editableElements,
        data,
        setData,
        attributes,
        setAttributes
    } = useMultiRootEditor( editorProps );

    return (
        <div className="App">
            { toolbarElement }
            { editableElements }
        </div>
    );
}

export default App;

After:

import { withCKEditorCloud } from '@ckeditor/ckeditor5-react';

/**
 * The `withCKEditorCloud` HOC allows you to load CKEditor 5 from the CKEditor Cloud and inject loaded data
 * as `cloud` property into your component. Configuration of the `cloud` passed to `withCKEditorCloud` is
 * the same as for the `useCKEditorCloud` hook and you can specify the version of CKEditor 5 to load and
 * optionally enable premium features.
 */
const withCKCloud = withCKEditorCloud( {
    cloud: {
        version: '43.0.0',
        languages: [ 'en', 'de' ]
    },

    // Optional. Render error when loading CKEditor 5 from the CKEditor Cloud fails.
    renderError: ( error ) => {
        console.error( error );

        return <div>Error!</div>;
    },

    // Optional: Render loading state when CKEditor 5 is being loaded from the CKEditor Cloud.
    renderLoader: () => <div>Loading...</div>,
} );

const App = withCKCloud( ( { cloud } ) => {
    const {
        MultiRootEditor,
        Bold,
        Essentials,
        Italic,
        Paragraph
    } = cloud.CKEditor;

    const editorProps = {
        editor: MultiRootEditor,
        data: {
            intro: '<h1>React multi-root editor</h1>',
            content: '<p>Hello from CKEditor&nbsp;5 multi-root!</p>'
        },
        config: {
            plugins: [ Essentials, Bold, Italic, Paragraph ],
            toolbar: {
                items: [ 'undo', 'redo', '|', 'bold', 'italic' ]
            },
        }
    };

    const {
        editor,
        toolbarElement,
        editableElements,
        data,
        setData,
        attributes,
        setAttributes
    } = useMultiRootEditor( editorProps );

    return (
        <div className="App">
            { toolbarElement }
            { editableElements }
        </div>
    );
} );