Framework Integration Guide
This guide shows how to integrate quikdown into popular JavaScript frameworks.
Table of Contents
React
Basic Usage
import React, { useState, useMemo } from 'react';
import quikdown from 'quikdown';
function MarkdownEditor() {
const [markdown, setMarkdown] = useState('# Hello React\n\nEdit me!');
const html = useMemo(() => {
return quikdown(markdown, { inline_styles: true });
}, [markdown]);
return (
<div style={{ display: 'flex', gap: '20px' }}>
<textarea
value={markdown}
onChange={(e) => setMarkdown(e.target.value)}
style={{ width: '50%', minHeight: '400px' }}
/>
<div
dangerouslySetInnerHTML={{ __html: html }}
style={{ width: '50%' }}
/>
</div>
);
}
export default MarkdownEditor;With Bidirectional Support
⚠️ Note: Bidirectional support requires quikdown_bd, not regular quikdown.
import React, { useState, useEffect, useRef } from 'react';
// Use quikdown_bd for bidirectional support, NOT regular quikdown
import quikdown_bd from 'quikdown/bd';
function BidirectionalEditor() {
const [markdown, setMarkdown] = useState('# Bidirectional Editor\n\n**Edit** either side!');
const [isEditingHtml, setIsEditingHtml] = useState(false);
const htmlRef = useRef(null);
useEffect(() => {
if (!isEditingHtml && htmlRef.current) {
htmlRef.current.innerHTML = quikdown_bd(markdown, { bidirectional: true });
}
}, [markdown, isEditingHtml]);
const handleHtmlEdit = () => {
if (htmlRef.current) {
const newMarkdown = quikdown_bd.toMarkdown(htmlRef.current);
setMarkdown(newMarkdown);
setIsEditingHtml(false);
}
};
return (
<div style={{ display: 'flex', gap: '20px' }}>
<textarea
value={markdown}
onChange={(e) => {
setMarkdown(e.target.value);
setIsEditingHtml(false);
}}
style={{ width: '50%', minHeight: '400px' }}
/>
<div
ref={htmlRef}
contentEditable
onFocus={() => setIsEditingHtml(true)}
onBlur={handleHtmlEdit}
style={{ width: '50%', border: '1px solid #ccc', padding: '10px' }}
/>
</div>
);
}Custom Hook
import { useState, useMemo } from 'react';
import quikdown from 'quikdown';
export function useMarkdown(initialMarkdown = '', options = {}) {
const [markdown, setMarkdown] = useState(initialMarkdown);
const html = useMemo(() => {
return quikdown(markdown, options);
}, [markdown, options]);
return { markdown, setMarkdown, html };
}
// Usage
function MyComponent() {
const { markdown, setMarkdown, html } = useMarkdown('# Hello');
// ...
}Vue.js
Vue 3 Composition API
<template>
<div class="markdown-editor">
<textarea
v-model="markdown"
class="editor"
/>
<div
v-html="html"
class="preview"
/>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
import quikdown from 'quikdown';
const markdown = ref('# Hello Vue 3\n\nEdit this **markdown**!');
const html = computed(() => {
return quikdown(markdown.value, { inline_styles: true });
});
</script>
<style scoped>
.markdown-editor {
display: flex;
gap: 20px;
}
.editor, .preview {
width: 50%;
min-height: 400px;
padding: 10px;
border: 1px solid #ccc;
}
</style>Vue 2 Options API
<template>
<div class="markdown-editor">
<textarea
v-model="markdown"
class="editor"
/>
<div
v-html="html"
class="preview"
/>
</div>
</template>
<script>
import quikdown from 'quikdown';
export default {
data() {
return {
markdown: '# Hello Vue 2\n\nEdit me!'
};
},
computed: {
html() {
return quikdown(this.markdown, { inline_styles: true });
}
}
};
</script>Vue Component with Bidirectional Support
<template>
<div class="bidirectional-editor">
<textarea
v-model="markdown"
@input="syncFromMarkdown"
class="editor"
/>
<div
ref="htmlEditor"
contenteditable="true"
@blur="syncFromHtml"
class="preview"
/>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import quikdown_bd from 'quikdown/bd';
const markdown = ref('# Bidirectional\n\n**Edit** either side!');
const htmlEditor = ref(null);
let isUpdating = false;
const syncFromMarkdown = () => {
if (!isUpdating && htmlEditor.value) {
isUpdating = true;
htmlEditor.value.innerHTML = quikdown_bd(markdown.value, { bidirectional: true });
isUpdating = false;
}
};
const syncFromHtml = () => {
if (!isUpdating && htmlEditor.value) {
isUpdating = true;
markdown.value = quikdown_bd.toMarkdown(htmlEditor.value);
isUpdating = false;
}
};
onMounted(() => {
syncFromMarkdown();
});
</script>Svelte
Basic Svelte Component
<script>
import quikdown from 'quikdown';
let markdown = '# Hello Svelte\n\n**Bold** and *italic* text';
$: html = quikdown(markdown, { inline_styles: true });
</script>
<div class="editor">
<textarea bind:value={markdown} />
<div class="preview">
{@html html}
</div>
</div>
<style>
.editor {
display: flex;
gap: 20px;
}
textarea, .preview {
width: 50%;
min-height: 400px;
padding: 10px;
border: 1px solid #ccc;
}
</style>Svelte Store Integration
// markdownStore.js
import { writable, derived } from 'svelte/store';
import quikdown from 'quikdown';
export function createMarkdownStore(initialValue = '') {
const markdown = writable(initialValue);
const html = derived(markdown, $markdown =>
quikdown($markdown, { inline_styles: true })
);
return {
markdown,
html,
setMarkdown: markdown.set,
updateMarkdown: markdown.update
};
}<!-- Component.svelte -->
<script>
import { createMarkdownStore } from './markdownStore.js';
const { markdown, html, setMarkdown } = createMarkdownStore('# Hello Store');
</script>
<textarea value={$markdown} on:input={(e) => setMarkdown(e.target.value)} />
<div>{@html $html}</div>Bidirectional Svelte Component
<script>
import { onMount } from 'svelte';
import quikdown_bd from 'quikdown/bd';
let markdown = '# Bidirectional\n\n**Edit** anywhere!';
let htmlElement;
let isUpdating = false;
function updateHtml() {
if (!isUpdating && htmlElement) {
isUpdating = true;
htmlElement.innerHTML = quikdown_bd(markdown, { bidirectional: true });
isUpdating = false;
}
}
function updateMarkdown() {
if (!isUpdating && htmlElement) {
isUpdating = true;
markdown = quikdown_bd.toMarkdown(htmlElement);
isUpdating = false;
}
}
onMount(() => {
updateHtml();
});
$: if (markdown) updateHtml();
</script>
<div class="editor">
<textarea bind:value={markdown} />
<div
bind:this={htmlElement}
contenteditable="true"
on:blur={updateMarkdown}
class="preview"
/>
</div>Angular
Angular Component
// markdown-editor.component.ts
import { Component, OnInit } from '@angular/core';
import quikdown from 'quikdown';
@Component({
selector: 'app-markdown-editor',
template: `
<div class="editor-container">
<textarea
[(ngModel)]="markdown"
(ngModelChange)="updateHtml()"
class="editor"
></textarea>
<div
[innerHTML]="html"
class="preview"
></div>
</div>
`,
styles: [`
.editor-container {
display: flex;
gap: 20px;
}
.editor, .preview {
width: 50%;
min-height: 400px;
padding: 10px;
border: 1px solid #ccc;
}
`]
})
export class MarkdownEditorComponent implements OnInit {
markdown = '# Hello Angular\n\n**Bold** text';
html = '';
ngOnInit() {
this.updateHtml();
}
updateHtml() {
this.html = quikdown(this.markdown, { inline_styles: true });
}
}Angular Service
// markdown.service.ts
import { Injectable } from '@angular/core';
import quikdown from 'quikdown';
import quikdown_bd from 'quikdown/bd';
@Injectable({
providedIn: 'root'
})
export class MarkdownService {
toHtml(markdown: string, options?: any): string {
return quikdown(markdown, options);
}
toHtmlBidirectional(markdown: string): string {
return quikdown_bd(markdown, { bidirectional: true });
}
toMarkdown(html: string | HTMLElement): string {
return quikdown_bd.toMarkdown(html);
}
}Angular Pipe
// markdown.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';
import quikdown from 'quikdown';
@Pipe({
name: 'markdown'
})
export class MarkdownPipe implements PipeTransform {
transform(value: string, options?: any): string {
return value ? quikdown(value, options) : '';
}
}
// Usage in template
// <div [innerHTML]="markdownContent | markdown"></div>Next.js
Next.js Page with SSR
// pages/markdown-demo.js
import { useState } from 'react';
import quikdown from 'quikdown';
// Server-side rendering
export async function getServerSideProps() {
const initialMarkdown = '# Server-Rendered\n\nThis was rendered on the server!';
const initialHtml = quikdown(initialMarkdown, { inline_styles: true });
return {
props: {
initialMarkdown,
initialHtml
}
};
}
export default function MarkdownDemo({ initialMarkdown, initialHtml }) {
const [markdown, setMarkdown] = useState(initialMarkdown);
const [html, setHtml] = useState(initialHtml);
const handleChange = (e) => {
const newMarkdown = e.target.value;
setMarkdown(newMarkdown);
setHtml(quikdown(newMarkdown, { inline_styles: true }));
};
return (
<div style={{ display: 'flex', gap: '20px', padding: '20px' }}>
<textarea
value={markdown}
onChange={handleChange}
style={{ width: '50%', minHeight: '400px' }}
/>
<div
dangerouslySetInnerHTML={{ __html: html }}
style={{ width: '50%' }}
/>
</div>
);
}Next.js API Route
// pages/api/markdown.js
import quikdown from 'quikdown';
export default function handler(req, res) {
if (req.method === 'POST') {
const { markdown, options = {} } = req.body;
try {
const html = quikdown(markdown, options);
res.status(200).json({ html });
} catch (error) {
res.status(400).json({ error: error.message });
}
} else {
res.status(405).json({ error: 'Method not allowed' });
}
}Nuxt
Nuxt 3 Component
<!-- components/MarkdownEditor.vue -->
<template>
<div class="markdown-editor">
<textarea
v-model="markdown"
class="editor"
/>
<div
v-html="html"
class="preview"
/>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
import quikdown from 'quikdown';
const markdown = ref('# Hello Nuxt 3');
const html = computed(() => quikdown(markdown.value, { inline_styles: true }));
</script>Nuxt Plugin
// plugins/quikdown.client.js
import quikdown from 'quikdown';
import quikdown_bd from 'quikdown/bd';
export default defineNuxtPlugin(() => {
return {
provide: {
quikdown,
quikdown_bd
}
};
});
// Usage in component
// const { $quikdown } = useNuxtApp();
// const html = $quikdown(markdown);Common Patterns
Debounced Updates
For better performance with large documents:
import { debounce } from 'lodash-es'; // or your debounce implementation
const debouncedConvert = debounce((markdown, callback) => {
const html = quikdown(markdown);
callback(html);
}, 300);Syntax Highlighting Integration
import quikdown from 'quikdown';
import hljs from 'highlight.js';
const highlightPlugin = {
render: (code, language) => {
if (language && hljs.getLanguage(language)) {
try {
const result = hljs.highlight(code, { language });
return `<pre><code class="hljs language-${language}">${result.value}</code></pre>`;
} catch (e) {
console.error('Highlighting failed:', e);
}
}
return undefined; // Use default
}
};
const html = quikdown(markdown, {
fence_plugin: highlightPlugin
});Sanitization
While quikdown has built-in XSS protection, you can add extra sanitization:
import quikdown from 'quikdown';
import DOMPurify from 'dompurify';
function safeRender(markdown) {
const html = quikdown(markdown);
return DOMPurify.sanitize(html);
}TypeScript Support
All frameworks can benefit from quikdown's TypeScript definitions:
import quikdown, { QuikdownOptions } from 'quikdown';
import quikdown_bd from 'quikdown/bd';
const options: QuikdownOptions = {
inline_styles: true,
fence_plugin: {
render: (code: string, lang: string) => {
// Custom plugin logic
return `<pre class="${lang}">${code}</pre>`;
}
}
};
const html: string = quikdown(markdown, options);
const backToMarkdown: string = quikdown_bd.toMarkdown(html);Performance Tips
- Memoize conversions - Use React.useMemo, Vue computed, or Svelte reactive statements
- Debounce updates - For live editors, debounce the conversion by 200-300ms
- Virtual scrolling - For very long documents, consider virtual scrolling
- Web Workers - For large documents, consider moving conversion to a Web Worker
- Lazy loading - Load quikdown dynamically when needed:
// Dynamic import
const quikdown = await import('quikdown');
const html = quikdown.default(markdown);