Anti patterns
19.2 Anti-Patterns & Common Mistakes
When to Use
You need to avoid common pitfalls and bad practices.
Anti-Patterns
1. Mutating Editor State Directly
❌ Wrong:
editor.state.doc = newDoc // NEVER mutate state directly
✓ Right:
editor.commands.setContent(newContent) // Use commands
Why: ProseMirror state is immutable. Direct mutation breaks transaction system, history, and collaboration.
2. Using HTML for Storage
❌ Wrong:
const html = editor.getHTML()
database.save(html) // Lossy, XSS risk
✓ Right:
const json = editor.getJSON()
database.save(json) // Lossless, structured, safe
Why: HTML loses schema information, risks XSS, and can't be reliably re-parsed. JSON is the canonical format.
3. Not Isolating Editor in React
❌ Wrong:
function App() {
const [theme, setTheme] = useState('light')
const editor = useEditor({ extensions: [StarterKit] })
return (
<>
<ThemeToggle onChange={setTheme} /> {/* Re-renders editor */}
<EditorContent editor={editor} />
</>
)
}
✓ Right:
function EditorComponent() {
const editor = useEditor({ extensions: [StarterKit] })
return <EditorContent editor={editor} />
}
function App() {
const [theme, setTheme] = useState('light')
return (
<>
<ThemeToggle onChange={setTheme} />
<EditorComponent /> {/* Isolated */}
</>
)
}
Why: Unrelated state changes cause expensive editor re-renders. Isolation prevents this.
4. Forgetting to Disable History with Collaboration
❌ Wrong:
new Editor({
extensions: [
StarterKit, // Includes History
Collaboration.configure({ document: ydoc }),
],
})
✓ Right:
new Editor({
extensions: [
StarterKit.configure({ history: false }), // Disable
Collaboration.configure({ document: ydoc }),
],
})
Why: Collaboration has its own Y.js-based history. StarterKit's history conflicts, causing undo/redo bugs.
5. Not Sanitizing Link URLs
❌ Wrong:
Link.configure({
// No validation
})
✓ Right:
Link.configure({
validate: (url) => /^https?:\/\//.test(url),
protocols: ['http', 'https'],
})
Why: javascript:, data:, and vbscript: URLs are XSS vectors (CVE-2025-14284). Always validate.
6. Using React Node Views for Simple Content
❌ Wrong:
addNodeView() {
return ReactNodeViewRenderer(({ node }) => (
<div>{node.attrs.text}</div>
))
}
✓ Right:
renderHTML({ node }) {
return ['div', node.attrs.text]
}
Why: React node views are 10x slower than HTML rendering. Use HTML unless you need interactivity.
7. Not Debouncing Auto-Save
❌ Wrong:
editor.on('update', ({ editor }) => {
saveToServer(editor.getJSON()) // Every keystroke!
})
✓ Right:
const debouncedSave = debounce((content) => {
saveToServer(content)
}, 1000)
editor.on('update', ({ editor }) => {
debouncedSave(editor.getJSON())
})
Why: Saving on every keystroke floods the server and degrades UX. Debounce to 1-2 seconds.
8. Expecting StarterKit to Include Everything
❌ Wrong:
new Editor({
extensions: [StarterKit],
})
editor.commands.setImage({ src: '...' }) // Fails - Image not in StarterKit
✓ Right:
import Image from '@tiptap/extension-image'
new Editor({
extensions: [StarterKit, Image],
})
Why: StarterKit is common extensions only. Link, Image, Table, Mention must be installed separately.
9. Not Setting enableContentCheck in Production
❌ Wrong:
new Editor({
extensions: [StarterKit],
// enableContentCheck defaults to false
})
✓ Right:
new Editor({
extensions: [StarterKit],
enableContentCheck: true, // Catch schema violations
onContentError({ editor, error, disableCollaboration }) {
console.error(error)
disableCollaboration() // Prevent syncing bad content
editor.setEditable(false)
},
})
Why: Invalid content silently breaks collaboration. enableContentCheck detects this early.
10. Using @extend in Custom CSS
❌ Wrong:
.custom-node {
@extend .tiptap; // Bloats selectors
}
✓ Right:
.custom-node {
@include tiptap-styles; // Use mixins
// Or just apply classes directly in HTML
}
Why: @extend causes selector explosion, making CSS hard to debug and slow to parse. Use mixins or utility classes.
See Also
- ← 19.1 Testing Strategy
- → 18.1 Security Model | XSS prevention
- Reference: https://liveblocks.io/docs/guides/tiptap-best-practices-and-tips