react-hook-quill
Version:
React Hook Quill is a lightweight wrapper for Quill, the rich text editor that does not interfere with the design of either React or Quill.
501 lines (421 loc) • 14.6 kB
Markdown
# React Hook Quill
React Hook Quill is a lightweight wrapper for [Quill](https://quilljs.com/) that does not interfere with the design of either React or Quill.
Quill is implemented without frameworks like React. To put it simply, this hook internally initializes Quill as an [external system](https://react.dev/reference/react/useEffect#connecting-to-an-external-system) within React using `useEffect` and cleans it up during a re-render and the unmount phase.
## Quick Start
### Install
```bash
npm install react-hook-quill
```
### Non-state Control of `Delta` with React using `useQuill` and `usePersistentDelta`
In this case, user edits are outside of the React lifecycle. React doesn't track the Quill changes, but user edits are automatically retained.
```tsx
import { memo, useRef } from 'react';
import 'quill/dist/quill.snow.css';
import { Delta } from 'quill';
import { useQuill, usePersistentDelta } from 'react-hook-quill';
// Use `memo` to avoid re-rendering when the parent component re-renders.
// This is for performance purposes only.
const Editor = memo(() => {
const ref = useRef<HTMLDivElement>(null);
const { persistentDeltaSetting } = usePersistentDelta(
{
containerRef: ref,
options: {
theme: 'snow'
},
setup: (quill) => {
// Disable undo for the initial text (optional).
quill.history.clear()
}
},
// Set an initial Delta (optional).
new Delta().insert('Hello Quill')
);
useQuill({ setting: persistentDeltaSetting });
return (
<div ref={ref} />
);
});
```
#### Reference
- memo: https://react.dev/reference/react/memo
### State Control of `Delta` with React using `useQuill` and `useSyncDelta`
`useSyncDelta` automatically sets up the state of `Delta` with React.
Note that you may not really need to sync `Delta` with React in your application.
Syncing `Delta` triggers a re-render with every user's edit and it may become an overhead in some cases.
```tsx
import { useRef } from 'react';
import { Delta } from 'quill';
import 'quill/dist/quill.snow.css';
import { useQuill, useSyncDelta } from 'react-hook-quill';
const Editor = () => {
const ref = useRef<HTMLDivElement>(null);
const { delta, syncDeltaSetting } = useSyncDelta(
{
containerRef: ref,
options: {
theme: 'snow'
},
setup: (quill) => {
// Disable undo for the initial text (optional).
quill.history.clear()
}
},
// Set an initial Delta (optional).
new Delta().insert('Hello Quill')
);
useQuill({
setting: syncDeltaSetting
});
return (
<>
<div ref={ref} />
<div>{JSON.stringify(delta)}</div>
</>
);
};
```
#### Reference
- Render and Commit: https://react.dev/learn/render-and-commit
## Access to the Quill instance
`Delta` is accessible via the return value of `useQuill` as a reference to the Quill instance.
```ts
...
const quillRef = useQuill({
setting: {
containerRef: ref
}
});
...
// This code must be inside useEffect or event handlers to avoid reading while rendering.
// See the pitfall of useRef: https://react.dev/reference/react/useRef#referencing-a-value-with-a-ref
const delta = quillRef.current?.editor.delta;
...
```
Quill API is fully accessible in the same way as mentioned above.
#### Reference
- Delta: https://quilljs.com/docs/delta
- Quill API: https://quilljs.com/docs/api
- Pitfall of useRef: https://react.dev/reference/react/useRef#referencing-a-value-with-a-ref
## Types and Interfaces
### Setting
A type for the parameter of `useQuill`.
```ts
export type Setting<ModuleOption = unknown> = {
/**
* A div element to attach a Quill Editor to
*/
containerRef: React.RefObject<HTMLDivElement | null>;
/**
* Options for initializing a Quill instance
* See: https://quilljs.com/docs/configuration#options
*/
options?: SafeQuillOptions<ModuleOption>;
/**
* This function is executed only once when Quill is mounted.
* A common use case is setting up synchronization of the Delta stored on the React side when the Quill side changes.
* You can read or write a ref object inside.
*/
setup?: (quill: Quill) => void;
/**
* This function is executed only once when Quill is unmounted.
* You can read or write a ref object inside.
*/
cleanup?: (quill: Quill) => void;
}
```
### SafeQuillOptions
It extends the type of `modules` in `QuillOptions` to explicitly specify its type using generics. It is originally `Record<string, unknown>`.
```ts
interface SafeQuillOptions<ModuleOption> extends QuillOptions {
modules?: Record<string, ModuleOption>;
}
```
#### Reference
- QuillOptions: https://quilljs.com/docs/configuration#options
- Modules: https://quilljs.com/docs/modules
## Hooks
### `useQuill`
It initializes Quill in a React component **after the DOM has been mounted**.
#### Parameters
| |arg |type | | |
|-|------------|----------------------|---------|-----------------------------------------|
|1|{ setting } |{ setting: `Setting` }|required | See the section of [setting](#setting). |
#### Returns
A reference to the Quill instance. Before it is instantiated, the ref points to `null`.
type: `React.MutableRefObject<Quill | null>`
----
### `usePersistentDelta`
#### Parameters
| |arg |type | | |
|-|------------|---------|---------|----------------------------------------|
|1|setting |`Setting`|required | See the section of [setting](#setting).|
|2|initialDelta|`Delta` |optional | The default value is `new Delta()` |
#### Returns
type:
```ts
{
persistentDeltaSetting: Setting<unknown>;
updateSetting: (setting: Setting) => void;
}
```
|key | |
|----------------------|-------------------------------------------------------------------------------------------------|
|persistentDeltaSetting| This is used for passing to `useQuill`. |
|updateSetting | Update `Setting`. It invokes a cleanup function of `useQuill` and creates a new Quill instance. |
----
### `useSyncDelta`
#### Parameters
| |arg |type | | |
|-|------------|---------|---------|----------------------------------------|
|1|setting |`Setting`|required | See the section of [setting](#setting).|
|2|initialDelta|`Delta` |optional | The default value is `new Delta()` |
#### Returns
type:
```ts
{
delta: Delta;
setDelta: React.Dispatch<React.SetStateAction<Delta>>;
syncDelta: (quill: Quill | null, delta: Delta) => void;
syncDeltaSetting: Setting<unknown>;
updateSetting: (setting: Setting) => void;
}
```
|key | |
|----------------|----------------------------------------------------------------------------------------------------------------------------------|
|delta | A state of `Delta` on the React side. User edits are automatically synced. |
|setDelta | **Minor use cases**. Note that it changes the state of `Delta` only on the React side. Use `syncDelta` if you update both sides. |
|syncDelta | Change the `Delta` both on the React and Quill sides at once. |
|syncDeltaSetting| This is used for passing to `useQuill`. |
|updateSetting | Update `Setting`. It invokes a cleanup function of `useQuill` and creates a new Quill instance. |
## More Examples
### Non-state Control of `Delta` with React
```tsx
import { memo, useRef } from 'react';
import { Delta } from 'quill';
import 'quill/dist/quill.snow.css';
import { useQuill } from 'react-hook-quill';
const Editor = memo(() => {
const ref = useRef<HTMLDivElement>(null);
// A reference to the delta that keeps user edits when the parent component re-renders.
const deltaRef = useRef<Delta | null>(null);
useQuill({
setting: {
containerRef: ref,
options: {
theme: 'snow'
},
setup: (quill) => {
// If previous user edits exist, set up the delta.
// You can read or write to the ref object because this function is called internally in `useEffect`.
// See the pitfall of useRef: https://react.dev/reference/react/useRef#referencing-a-value-with-a-ref
if (deltaRef.current) {
quill.setContents(deltaRef.current);
}
},
cleanup: (quill) => {
// Save user edits when it cleans up.
// It's the same as `setup`, you can read or write a ref object.
deltaRef.current = quill.editor.delta;
}
}
});
return (
<div ref={ref} />
);
});
```
#### Reference
- memo: https://react.dev/reference/react/memo
- Pitfall of useRef: https://react.dev/reference/react/useRef#referencing-a-value-with-a-ref
### Configure options
```tsx
import { memo, useRef } from 'react';
import 'quill/dist/quill.snow.css';
import { useQuill, usePersistentDelta } from 'react-hook-quill';
// https://quilljs.com/docs/modules/toolbar
const toolbarOptions = [
['bold', 'italic', 'underline', 'strike'],
['blockquote', 'code-block'],
['link', 'image', 'video', 'formula'],
[{ header: 1 }, { header: 2 }],
[{ list: 'ordered' }, { list: 'bullet' }, { list: 'check' }],
[{ script: 'sub' }, { script: 'super' }],
[{ indent: '-1' }, { indent: '+1' }],
[{ direction: 'rtl' }],
[{ size: ['small', false, 'large', 'huge'] }],
[{ header: [1, 2, 3, 4, 5, 6, false] }],
[{ color: [] }, { background: [] }],
[{ font: [] }],
[{ align: [] }],
['clean']
];
const Editor = memo(() => {
const ref = useRef<HTMLDivElement>(null);
const { persistentDeltaSetting } = usePersistentDelta({
containerRef: ref,
options: {
theme: 'snow',
placeholder: 'Enter some text...',
modules: {
toolbar: toolbarOptions
}
}
});
useQuill({ setting: persistentDeltaSetting });
return (
<div ref={ref} />
);
});
```
### Configure modules
```tsx
import { memo, useRef } from 'react';
import 'quill/dist/quill.snow.css';
import Quill, { Module } from 'quill';
import { useQuill, usePersistentDelta, Setting } from 'react-hook-quill';
interface CounterModuleOptions {
container: '#counter';
unit: 'word' | 'character';
}
// https://quilljs.com/docs/guides/building-a-custom-module
class Counter extends Module<CounterModuleOptions> {
public quill: Quill;
public options: CounterModuleOptions;
constructor (quill: Quill, options: CounterModuleOptions) {
super(quill, options);
this.quill = quill;
this.options = options;
quill.on(Quill.events.TEXT_CHANGE, this.update.bind(this));
}
calculate () {
const text = this.quill.getText();
if (this.options.unit === 'word') {
const trimmed = text.trim();
return trimmed.split(/\s+/).length;
} else {
return text.length;
}
}
update () {
const length = this.calculate();
let label = this.options.unit;
if (length !== 1) {
label += 's';
}
const container = document.querySelector(this.options.container);
if (container) {
container.textContent = `${length} ${label}`;
}
}
}
Quill.register('modules/counter', Counter);
const Editor = memo(() => {
const ref = useRef<HTMLDivElement>(null);
const setting: Setting<CounterModuleOptions> = {
containerRef: ref,
options: {
theme: 'snow',
modules: {
counter: {
container: '#counter',
unit: 'character'
}
}
}
};
const { persistentDeltaSetting } = usePersistentDelta(setting);
useQuill({ setting: persistentDeltaSetting });
return (
<>
<div ref={ref} />
<div id='counter' />
</>
);
});
```
### Configure blots
```tsx
import { memo, useRef } from 'react';
import { useQuill, usePersistentDelta } from 'react-hook-quill';
import Quill from 'quill';
import { BlockEmbed } from 'quill/blots/block';
import 'quill/dist/quill.snow.css';
type DividerValue = 'blue' | 'red';
class DividerBlot extends BlockEmbed {
static blotName = 'divider';
static tagName = 'hr';
static create (value: DividerValue) {
const node = super.create(value);
if (node instanceof HTMLElement) {
node.setAttribute('style', `border: 1px solid ${value};`);
}
return node;
}
}
Quill.register(DividerBlot);
const Editor = memo(() => {
const ref = useRef<HTMLDivElement>(null);
const { persistentDeltaSetting } = usePersistentDelta(
{
containerRef: ref,
options: {
theme: 'snow'
}
}
);
const quillRef = useQuill({ setting: persistentDeltaSetting });
return (
<>
<div ref={ref} />
<button onClick={() => {
const quill = quillRef.current;
if (quill) {
const range = quill.getSelection(true);
const dividerValue: DividerValue = 'blue';
quill.insertText(range.index, '\n', Quill.sources.USER);
quill.insertEmbed(range.index + 1, 'divider', dividerValue, Quill.sources.USER);
quill.setSelection(range.index + 2, Quill.sources.SILENT);
}
}}>
Add Divider
</button>
</>
);
});
```
### Update settings
```tsx
import { memo, useRef, useState } from 'react';
import 'quill/dist/quill.snow.css';
import 'quill/dist/quill.bubble.css';
import { useQuill, usePersistentDelta } from 'react-hook-quill';
export const Editor = memo(() => {
const ref = useRef<HTMLDivElement>(null);
const [theme, setTheme] = useState('snow');
const { persistentDeltaSetting, updateSetting } = usePersistentDelta({
containerRef: ref,
options: {
theme: 'snow'
}
});
useQuill({ setting: persistentDeltaSetting });
return (
<>
<div ref={ref} />
<button onClick={() => {
const nextTheme = theme === 'snow' ? 'bubble' : 'snow';
updateSetting({
containerRef: ref,
options: {
theme: nextTheme
}
});
setTheme(nextTheme);
}}>
Change the theme
</button>
</>
);
});
```