Overview
Integrating a model-based editor with frameworks requires careful handling of lifecycle, state management, and DOM updates. The key is to let the editor manage its own DOM while integrating with framework state.
Key principles:
- Editor manages its own DOM - don't let framework re-render it
- Use refs to access editor instance
- Sync editor state with framework state when needed
- Handle lifecycle properly - initialize and cleanup
- Prevent framework from diffing editor DOM
React Integration
React integration requires using refs and preventing React from re-rendering the editor DOM:
Basic Integration
import { useEffect, useRef, useState } from 'react';
import { Editor } from './editor';
function EditorComponent() {
const editorRef = useRef<HTMLDivElement>(null);
const editorInstanceRef = useRef<Editor | null>(null);
const [content, setContent] = useState('');
useEffect(() => {
if (!editorRef.current) return;
// Initialize editor
const editor = new Editor({
element: editorRef.current,
initialContent: content,
});
editorInstanceRef.current = editor;
// Listen to changes
editor.on('change', (newContent) => {
setContent(newContent);
});
// Cleanup
return () => {
editor.destroy();
};
}, []); // Only run once
// Update editor when content prop changes (from parent)
useEffect(() => {
if (editorInstanceRef.current && content !== editorInstanceRef.current.getContent()) {
editorInstanceRef.current.setContent(content);
}
}, [content]);
return (
<div
ref={editorRef}
contentEditable={false} // Let editor manage contenteditable
suppressContentEditableWarning // React warning suppression
/>
);
}Custom Hook
import { useEffect, useRef, useState, useCallback } from 'react';
import { Editor } from './editor';
function useEditor(initialContent: string = '') {
const editorRef = useRef<HTMLDivElement>(null);
const editorInstanceRef = useRef<Editor | null>(null);
const [content, setContent] = useState(initialContent);
const [isReady, setIsReady] = useState(false);
useEffect(() => {
if (!editorRef.current) return;
const editor = new Editor({
element: editorRef.current,
initialContent,
});
editorInstanceRef.current = editor;
setIsReady(true);
editor.on('change', (newContent) => {
setContent(newContent);
});
return () => {
editor.destroy();
setIsReady(false);
};
}, []);
const setEditorContent = useCallback((newContent: string) => {
if (editorInstanceRef.current) {
editorInstanceRef.current.setContent(newContent);
setContent(newContent);
}
}, []);
const getEditorContent = useCallback(() => {
return editorInstanceRef.current?.getContent() || '';
}, []);
return {
editorRef,
content,
isReady,
setContent: setEditorContent,
getContent: getEditorContent,
editor: editorInstanceRef.current,
};
}
// Usage
function MyEditor() {
const { editorRef, content, isReady, setContent } = useEditor('');
return (
<div>
<div ref={editorRef} suppressContentEditableWarning />
{isReady && <p>Editor is ready. Content: {content}</p>}
</div>
);
}Preventing Re-renders
import { memo, useRef, useEffect } from 'react';
// Memoize to prevent unnecessary re-renders
const EditorComponent = memo(({ initialContent }: { initialContent: string }) => {
const editorRef = useRef<HTMLDivElement>(null);
const editorInstanceRef = useRef<Editor | null>(null);
useEffect(() => {
if (!editorRef.current) return;
const editor = new Editor({
element: editorRef.current,
initialContent,
});
editorInstanceRef.current = editor;
return () => {
editor.destroy();
};
}, []); // Empty deps - only initialize once
// Use data attribute to prevent React from diffing
return (
<div
ref={editorRef}
data-editor-root
suppressContentEditableWarning
// Prevent React from updating this element
dangerouslySetInnerHTML={{ __html: '' }}
/>
);
});
// Or use shouldComponentUpdate equivalent
function EditorWrapper({ content }: { content: string }) {
const editorRef = useRef<HTMLDivElement>(null);
const editorInstanceRef = useRef<Editor | null>(null);
useEffect(() => {
if (!editorRef.current) return;
const editor = new Editor({
element: editorRef.current,
});
editorInstanceRef.current = editor;
return () => editor.destroy();
}, []);
// Only update if content actually changed
useEffect(() => {
if (editorInstanceRef.current) {
const current = editorInstanceRef.current.getContent();
if (current !== content) {
editorInstanceRef.current.setContent(content);
}
}
}, [content]);
return <div ref={editorRef} suppressContentEditableWarning />;
}Vue Integration
Vue integration uses template refs and lifecycle hooks:
Composition API
<template>
<div ref="editorRef" />
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from "vue";
import { Editor } from './editor';
const editorRef = ref<HTMLElement | null>(null);
const editorInstance = ref<Editor | null>(null);
const content = ref('');
onMounted(() => {
if (!editorRef.value) return;
const editor = new Editor({
element: editorRef.value,
initialContent: content.value,
});
editorInstance.value = editor;
editor.on('change', (newContent) => {
content.value = newContent;
});
});
onUnmounted(() => {
editorInstance.value?.destroy();
});
// Watch for external content changes
watch(() => props.content, (newContent) => {
if (editorInstance.value && newContent !== editorInstance.value.getContent()) {
editorInstance.value.setContent(newContent);
}
});
</script>Options API
<template>
<div ref="editor" />
</template>
<script>
import { Editor } from './editor';
export default {
data() {
return {
editorInstance: null,
content: '',
};
},
mounted() {
if (!this.$refs.editor) return;
this.editorInstance = new Editor({
element: this.$refs.editor,
initialContent: this.content,
});
this.editorInstance.on('change', (newContent) => {
this.content = newContent;
});
},
beforeUnmount() {
if (this.editorInstance) {
this.editorInstance.destroy();
}
},
watch: {
// Watch for prop changes
initialContent(newContent) {
if (this.editorInstance && newContent !== this.editorInstance.getContent()) {
this.editorInstance.setContent(newContent);
}
},
},
};
</script>Svelte Integration
Svelte integration uses bind:this and lifecycle functions:
<script lang="ts">
import { onMount, onDestroy } from "svelte";
import { Editor } from './editor';
let editorElement: HTMLDivElement;
let editorInstance: Editor | null = null;
let content = '';
onMount(() => {
if (!editorElement) return;
editorInstance = new Editor({
element: editorElement,
initialContent: content,
});
editorInstance.on('change', (newContent) => {
content = newContent;
});
});
onDestroy(() => {
editorInstance?.destroy();
});
// Reactive statement for prop updates
$: if (editorInstance && $initialContent !== editorInstance.getContent()) {
editorInstance.setContent($initialContent);
}
</script>
<div bind:this={editorElement} />Common Patterns
Common patterns across frameworks:
Controlled vs Uncontrolled
// Controlled: Framework manages content
function ControlledEditor({ content, onChange }: ControlledProps) {
const editorRef = useRef<HTMLDivElement>(null);
const editorInstanceRef = useRef<Editor | null>(null);
useEffect(() => {
if (!editorRef.current) return;
const editor = new Editor({
element: editorRef.current,
initialContent: content,
});
editorInstanceRef.current = editor;
editor.on('change', (newContent) => {
onChange(newContent); // Notify parent
});
return () => editor.destroy();
}, []);
// Update editor when content prop changes
useEffect(() => {
if (editorInstanceRef.current) {
editorInstanceRef.current.setContent(content);
}
}, [content]);
return <div ref={editorRef} suppressContentEditableWarning />;
}
// Uncontrolled: Editor manages its own state
function UncontrolledEditor({ initialContent }: UncontrolledProps) {
const editorRef = useRef<HTMLDivElement>(null);
const editorInstanceRef = useRef<Editor | null>(null);
useEffect(() => {
if (!editorRef.current) return;
const editor = new Editor({
element: editorRef.current,
initialContent,
});
editorInstanceRef.current = editor;
return () => editor.destroy();
}, []);
// Expose methods via ref
useImperativeHandle(ref, () => ({
getContent: () => editorInstanceRef.current?.getContent() || '',
setContent: (content: string) => editorInstanceRef.current?.setContent(content),
}));
return <div ref={editorRef} suppressContentEditableWarning />;
}Preventing Framework DOM Diffing
// React: Use key to prevent re-renders
<div key="editor-root" ref={editorRef} suppressContentEditableWarning />
// React: Use data attribute
<div data-editor-root ref={editorRef} suppressContentEditableWarning />
// Vue: Use key
<div :key="'editor'" ref="editorRef" />
// Vue: Use v-once (render once)
<div v-once ref="editorRef" />
// Svelte: Use key
<div key="editor" bind:this={editorElement} />
// General: Mark element as non-reactive
<div
ref={editorRef}
data-framework-ignore // Framework should ignore this element
suppressContentEditableWarning
/>State Management
Integrating with state management libraries:
Redux Integration
import { useEffect, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Editor } from './editor';
import { setEditorContent, selectEditorContent } from './editorSlice';
function ReduxEditor() {
const editorRef = useRef<HTMLDivElement>(null);
const editorInstanceRef = useRef<Editor | null>(null);
const dispatch = useDispatch();
const content = useSelector(selectEditorContent);
useEffect(() => {
if (!editorRef.current) return;
const editor = new Editor({
element: editorRef.current,
initialContent: content,
});
editorInstanceRef.current = editor;
editor.on('change', (newContent) => {
dispatch(setEditorContent(newContent));
});
return () => editor.destroy();
}, []);
// Update editor when Redux state changes
useEffect(() => {
if (editorInstanceRef.current && content !== editorInstanceRef.current.getContent()) {
editorInstanceRef.current.setContent(content);
}
}, [content]);
return <div ref={editorRef} suppressContentEditableWarning />;
}Zustand Integration
import { useEffect, useRef } from 'react';
import { useEditorStore } from './editorStore';
import { Editor } from './editor';
function ZustandEditor() {
const editorRef = useRef<HTMLDivElement>(null);
const editorInstanceRef = useRef<Editor | null>(null);
const content = useEditorStore((state) => state.content);
const setContent = useEditorStore((state) => state.setContent);
useEffect(() => {
if (!editorRef.current) return;
const editor = new Editor({
element: editorRef.current,
initialContent: content,
});
editorInstanceRef.current = editor;
editor.on('change', (newContent) => {
setContent(newContent);
});
return () => editor.destroy();
}, []);
useEffect(() => {
if (editorInstanceRef.current && content !== editorInstanceRef.current.getContent()) {
editorInstanceRef.current.setContent(content);
}
}, [content]);
return <div ref={editorRef} suppressContentEditableWarning />;
}