Skip to content

Troubleshooting

Common issues and solutions when using Editu.

Editor Not Rendering

CSS Not Loaded

If the editor appears unstyled or broken, ensure you import the CSS:

typescript
// Import Editu styles
import '@editu/core/styles.css';

// For components like Bubble Menu (optional)
import '@editu/core/components.css';

// For mathematics support (optional)
import '@editu/core/mathematics.css';

Editor Instance is Null

The editor hook/composable/rune returns null until initialization completes:

tsx
import { useEdituEditor, EdituEditor } from '@editu/react';

function Editor() {
  const editor = useEdituEditor({});

  // Handle loading state
  if (!editor) {
    return <div>Loading...</div>;
  }

  return <EdituEditor editor={editor} />;
}

Console Errors

Check your browser console for initialization errors. Common issues include:

  • Missing peer dependencies
  • Extension conflicts
  • Invalid initial content format

Image Upload Failures

Upload Handler Not Configured

By default, Editu converts images to Base64. For production, you should configure a custom upload handler:

typescript
const editor = useEdituEditor({
  features: {
    image: {
      onUpload: async (file) => {
        const formData = new FormData();
        formData.append('image', file);

        const response = await fetch('/api/upload', {
          method: 'POST',
          body: formData,
        });

        if (!response.ok) {
          throw new Error('Upload failed');
        }

        const { url } = await response.json();
        return url;
      },
    },
  },
});

File Size Exceeded

You can configure maxFileSize and handle validation errors:

typescript
const editor = useEdituEditor({
  features: {
    image: {
      maxFileSize: 5 * 1024 * 1024, // 5MB
      onValidationError: (error) => {
        if (error.type === 'file_too_large') {
          alert(`File too large: ${error.message}`);
        }
      },
    },
  },
});

Invalid File Type

You can configure allowed MIME types:

typescript
const editor = useEdituEditor({
  features: {
    image: {
      allowedTypes: ['image/jpeg', 'image/png', 'image/webp'],
      onValidationError: (error) => {
        if (error.type === 'invalid_type') {
          alert('Only JPEG, PNG, and WebP images are allowed');
        }
      },
    },
  },
});

Network Errors

You should handle upload failures gracefully:

typescript
const editor = useEdituEditor({
  features: {
    image: {
      onUpload: async (file) => {
        // Your upload logic
      },
      onUploadError: (error, file) => {
        console.error(`Failed to upload ${file.name}:`, error);
        // Show user-friendly error message
        alert('Upload failed. Please check your connection and try again.');
      },
    },
  },
});

Performance Issues

Auto-Save Too Frequent

You can increase the debounce time for auto-save:

typescript
import { useEdituAutoSave } from '@editu/react';

useEdituAutoSave({
  getEditor: () => editor,
  onSave: async (content) => {
    await saveToServer(content);
  },
  debounceMs: 2000, // Wait 2 seconds after last change
});

Large Documents

For large documents, consider the following approaches:

  1. Disable unused features to reduce bundle size and processing overhead:
typescript
const editor = useEdituEditor({
  features: {
    mathematics: false,  // Disable if not needed
    diagram: false,      // Disable if not needed
    embed: false,        // Disable if not needed
  },
});
  1. Lazy load optional features:
typescript
// Only import mathematics CSS when needed
if (useMathematics) {
  import('@editu/core/mathematics.css');
}
  1. Optimize code blocks with fewer languages:
typescript
import { createLowlight, common } from 'lowlight';

// Use only common languages instead of all
const lowlight = createLowlight(common);

const editor = useEdituEditor({
  features: {
    codeBlock: {
      lowlight,
    },
  },
});

Sluggish Typing

If typing feels slow:

  1. Check for expensive onUpdate handlers
  2. Debounce your state updates
  3. Avoid synchronous JSON serialization on every keystroke
typescript
// Bad: Expensive on every keystroke
onUpdate: ({ editor }) => {
  setContent(editor.getJSON()); // Triggers re-render
  saveToLocalStorage(editor.getJSON()); // Synchronous I/O
},

// Good: Debounced updates
const [content, setContent] = useState(null);
const debouncedSetContent = useMemo(
  () => debounce((json) => setContent(json), 300),
  []
);

onUpdate: ({ editor }) => {
  debouncedSetContent(editor.getJSON());
},

Common Error Messages

"Cannot read property 'commands' of null"

The editor instance is not yet initialized. Always check for null:

typescript
// Wrong
editor.commands.setContent(content);

// Correct
if (editor) {
  editor.commands.setContent(content);
}

// Or use optional chaining
editor?.commands.setContent(content);

"Maximum call stack size exceeded"

This error is usually caused by:

  1. Circular content updates: Do not update content inside onUpdate:
typescript
// Wrong: Causes infinite loop
onUpdate: ({ editor }) => {
  editor.commands.setContent(transformContent(editor.getJSON()));
},

// Correct: Use external trigger
const handleTransform = () => {
  if (editor) {
    editor.commands.setContent(transformContent(editor.getJSON()));
  }
};
  1. Recursive component rendering: Verify that you set proper dependency arrays in hooks.

"Extension not found"

Verify that you enabled the required features:

typescript
// Error when trying to use disabled feature
editor.commands.toggleMathInline(); // Fails if mathematics: false

// Solution: Enable the feature
const editor = useEdituEditor({
  features: {
    mathematics: true,
  },
});

"Adding different instances of a keyed plugin"

This error occurs when multiple instances of the same ProseMirror plugin are loaded. Common causes:

  1. Duplicate Tiptap packages in dependencies:
bash
# Check for duplicates
pnpm why @tiptap/core
npm ls @tiptap/core
  1. Incorrect bundling of @editu/core:

Make sure your bundler treats @tiptap/* as external. For Vite:

typescript
// vite.config.ts
export default defineConfig({
  optimizeDeps: {
    exclude: ['@tiptap/core', '@tiptap/pm'],
  },
});
  1. Multiple editor instances sharing extensions:

You must create fresh extension instances for each editor:

typescript
// Wrong: Sharing extensions
const extensions = createEdituExtensions({});
const editor1 = useEdituEditor({ extensions });
const editor2 = useEdituEditor({ extensions }); // Error!

// Correct: Create extensions per editor
const editor1 = useEdituEditor({
  extensions: createEdituExtensions({}),
});
const editor2 = useEdituEditor({
  extensions: createEdituExtensions({}),
});

Framework-Specific Issues

React Hydration Errors

When you use SSR (Next.js, Remix), you must render the editor client-side only:

tsx
// Next.js
'use client';

import dynamic from 'next/dynamic';

const Editor = dynamic(() => import('./Editor'), { ssr: false });

export default function Page() {
  return <Editor />;
}
tsx
// Remix
import { ClientOnly } from 'remix-utils/client-only';

export default function Page() {
  return (
    <ClientOnly fallback={<div>Loading editor...</div>}>
      {() => <Editor />}
    </ClientOnly>
  );
}

Debugging Tips

Browser DevTools

  1. Inspect editor state:
javascript
// In browser console
const editor = document.querySelector('.editu-editor').__edituEditor;
console.log(editor.getJSON());
console.log(editor.state);
  1. Check registered extensions:
javascript
console.log(editor.extensionManager.extensions.map(e => e.name));
  1. Monitor transactions:
typescript
const editor = useEdituEditor({
  onTransaction: ({ transaction }) => {
    console.log('Transaction:', transaction);
    console.log('Steps:', transaction.steps);
  },
});

Editor State Inspection

Use getEdituEditorState for debugging:

typescript
import { getEdituEditorState } from '@editu/core';

const debugEditor = () => {
  if (!editor) return;

  const state = getEdituEditorState(editor);
  console.table({
    focused: state.isFocused,
    empty: state.isEmpty,
    canUndo: state.canUndo,
    canRedo: state.canRedo,
    characters: state.characterCount,
    words: state.wordCount,
  });
};

ProseMirror DevTools

Install ProseMirror DevTools for advanced debugging:

typescript
import { applyDevTools } from 'prosemirror-dev-tools';

const editor = useEdituEditor({
  onCreate: ({ editor }) => {
    if (process.env.NODE_ENV === 'development') {
      applyDevTools(editor.view);
    }
  },
});

Logging Transactions

Track all document changes:

typescript
const editor = useEdituEditor({
  onTransaction: ({ transaction, editor }) => {
    if (transaction.docChanged) {
      console.group('Document Changed');
      console.log('From:', transaction.before.toJSON());
      console.log('To:', editor.state.doc.toJSON());
      console.log('Steps:', transaction.steps.map(s => s.toJSON()));
      console.groupEnd();
    }
  },
});

Getting Help

If you encounter an issue not covered here:

  1. Search existing issues: GitHub Issues
  2. Check Tiptap documentation: Tiptap Docs
  3. Open a new issue: Include:
    • Editu version
    • Framework and version
    • Minimal reproduction code
    • Expected vs actual behavior
    • Browser and OS

Next Steps

Released under the MIT License.