react-quill
react-quill is a React 16 through 18 wrapper around Quill 1.3.7. It renders a browser rich-text editor, exposes Quill through a component ref, accepts HTML or Delta documents, and maps React props to themes, formats, toolbar modules, selection events, and read-only mode. Its controlled mode is only a hybrid because Quill mutates its own DOM before React can reconcile it. The current README now says most React users probably do not need this wrapper and links to Quill's official direct-React example.
Do not install react-quill for a new application. Keep it only as a legacy compatibility layer around Quill 1, then choose direct Quill 2, a maintained fork, or a different editor when you can budget the migration.
Use it if
- You maintain an existing React 16, 17, or 18 application already storing Quill 1 Delta documents
- You need a low-risk patch to a working ReactQuill v2 integration and cannot migrate editor data or toolbar code now
- You depend on ReactQuill's component ref and callback shape and have tests covering its hybrid controlled behavior
- You are starting a new editor integration: the project's 2025 README says you probably do not need this library and points to using Quill directly with React
- You use React 19 or current Quill 2: version 2.0.0 declares peer support only through React 18 and pins Quill 1.3.7, while the standalone quill package is already 2.0.3
- You want active maintenance: the npm release dates to August 2022, the repository has not been pushed since February 2025, and 431 issues and PRs remain open
- You need true controlled-component semantics or server rendering: Quill owns DOM state, react-quill calls its mode semi-controlled, and the editor requires a browser DOM
Setup reality
Install react-quill beside supported React and React DOM versions, then import a Quill theme stylesheet or toolbars and popups will look broken. The package pins the old Quill 1 line and does not declare React 19 support. Browser-only code needs client-side or dynamic loading in SSR frameworks. You must also choose HTML versus Delta storage, avoid feeding change-only Deltas back as value, sanitize HTML at trust boundaries, and keep module configuration stable across renders.
Patterns
Use the hybrid controlled HTML modecontrolled-html-editor
import { useState } from 'react'
import ReactQuill from 'react-quill'
import 'react-quill/dist/quill.snow.css'
export function Editor() {
const [html, setHtml] = useState('')
return <ReactQuill theme="snow" value={html} onChange={setHtml} />
}ReactQuill cannot prevent Quill's DOM edit first, so value can overwrite content afterward; it is not a normal fully controlled input.
Avoid repeated content reconciliationuncontrolled-editor
<ReactQuill
theme="snow"
defaultValue="<p>Draft text</p>"
onChange={html => saveDraft(html)}
/>defaultValue is read only during initialization. Later prop changes do not reset the document.
Store the complete Delta documentstore-full-delta
const [document, setDocument] = useState({ ops: [] })
<ReactQuill
value={document}
onChange={(_html, _change, _source, editor) => {
setDocument(editor.getContents())
}}
/>Never pass the second onChange argument back as value; it contains only the last change and can cause an update loop.
Limit toolbar controls and accepted formatslimit-formats
const modules = {
toolbar: [
[{ header: [1, 2, false] }],
['bold', 'italic', 'underline'],
[{ list: 'ordered' }, { list: 'bullet' }],
['link', 'clean'],
],
}
const formats = ['header', 'bold', 'italic', 'underline', 'list', 'bullet', 'link']
<ReactQuill modules={modules} formats={formats} />Formats omitted from formats are not accepted even if a toolbar button or pasted content tries to apply them.
Use the imperative Quill instanceaccess-quill-instance
import { useRef } from 'react'
const quillRef = useRef(null)
function focusEditor() {
quillRef.current?.getEditor().focus()
}
<ReactQuill ref={quillRef} defaultValue="" />getEditor exposes the mutable Quill instance. Prefer the callback editor proxy for reads and use the full instance only when props cannot express the operation.
Track selection changesread-selection
<ReactQuill
onChangeSelection={(range, source, editor) => {
if (range) {
console.log(range.index, range.length, source, editor.getText(range.index, range.length))
}
}}
/>range is null when the editor loses focus, so guard it before reading index or length.
Switch to read-only modetoggle-read-only
<ReactQuill
theme="snow"
value={html}
readOnly={!canEdit}
modules={{ toolbar: canEdit }}
/>readOnly disables editing, but your application must still enforce authorization when saving content.
Add a custom toolbar actionadd-toolbar-handler
function insertDivider() {
const range = this.quill.getSelection(true)
this.quill.insertText(range.index, '\n---\n', 'user')
}
const modules = {
toolbar: {
container: [['bold', 'italic'], ['divider']],
handlers: { divider: insertDivider },
},
}Use a normal function when the handler needs this.quill; an arrow function does not receive the toolbar module's this value.
Load ReactQuill only in a Next.js browserload-without-ssr
import dynamic from 'next/dynamic'
const ReactQuill = dynamic(() => import('react-quill'), { ssr: false })
export function ClientEditor(props) {
return <ReactQuill theme="snow" {...props} />
}This avoids evaluating the DOM-dependent editor on the server; import the theme CSS from a location your Next.js setup allows.
Read plain text separately from HTMLget-plain-text
<ReactQuill
onChange={(html, _delta, _source, editor) => {
save({ html, text: editor.getText() })
}}
/>Quill's plain text generally includes a trailing newline; normalize it only if your storage or character-count rules require that.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| quill | npm | You want current Quill 2 and can follow its official direct React integration without a wrapper |
| react-quill-new | npm | You need a community-maintained ReactQuill-style fork with newer React and Quill compatibility |
| @tiptap/react | npm | You want an actively developed headless editor with deeper extension and schema control |