5

I'm running into a funny problem. I'm using NextJS for its server-side rendering capabilities and am using ReactQuill as my rich-text editor. To get around ReactQuill's tie to the DOM, I'm dynamically importing it. However, that presents another problem which is that when I try to attach a ref to the ReactQuill component, it's treated as a loadable component instead of the ReactQuill component. I need the ref in order to customize how images are handled when uploaded into the rich-text editor. Right now, the ref returns current:null instead of the function I can use .getEditor() on to customize image handling.

Anybody have any thoughts on how I can address this? I tried ref-forwarding, but it's still applying refs to a loadable component, instead of the React-Quill one. Here's a snapshot of my code.

const ReactQuill = dynamic(import('react-quill'), { ssr: false, loading: () => <p>Loading ...</p> }
);

const ForwardedRefComponent = React.forwardRef((props, ref) => {return (
    <ReactQuill {...props} forwardedRef={(el) => {ref = el;}} />
)})

class Create extends Component {
    constructor() {
        super();
        this.reactQuillRef = React.createRef();
    }

    imageHandler = () => {
         console.log(this.reactQuillRef); //this returns current:null, can't use getEditor() on it.
    }
    render() {
    const modules = {
      toolbar: {
          container:  [[{ 'header': [ 2, 3, false] }],
            ['bold', 'italic', 'underline', 'strike'],
            [{ 'list': 'ordered'}, { 'list': 'bullet' }],
            [{ 'script': 'sub'}, { 'script': 'super' }],
            ['link', 'image'],
            [{ 'indent': '-1'}, { 'indent': '+1' }],    
            [{ 'align': [] }],
            ['blockquote', 'code-block'],],
          handlers: {
             'image': this.imageHandler
          }
        }
     };
         return(
             <ForwardedRefComponent 
                value={this.state.text}
                onChange={this.handleChange}
                modules={modules}
                ref={this.reactQuillRef}/> //this.reactQuillRef is returning current:null instead of the ReactQuill function for me to use .getEditor() on
         )
    }
}

const mapStateToProps = state => ({
    tutorial: state.tutorial,
});

export default connect(
    mapStateToProps, {createTutorial}
)(Create);
user3783615
  • 81
  • 1
  • 1
  • 3

4 Answers4

7

I share my solution with hope that it helps you too.

Helped from https://github.com/zenoamaro/react-quill/issues/642#issuecomment-717661518

const ReactQuill = dynamic(
  async () => {
    const { default: RQ } = await import("react-quill");

    return ({ forwardedRef, ...props }) => <RQ ref={forwardedRef} {...props} />;
  },
  {
    ssr: false
  }
);


export default function QuillWrapper() {
  const quillRef = React.useRef(false)

  return <>
    <ReactQuill forwardedRef={quillRef} />
  </>
}

for example you can use the ref to upload image with custom hanlder

import React, { useMemo } from "react";
import dynamic from "next/dynamic";

const ReactQuill = dynamic(
  async () => {
const { default: RQ } = await import("react-quill");

return ({ forwardedRef, ...props }) => <RQ ref={forwardedRef} {...props} />;
  },
  {
ssr: false,
  }
);

export default function QuillWrapper({ value, onChange, ...props }) {
  const quillRef = React.useRef(false);

  // Custom image upload handler
  function imgHandler() {
// from https://github.com/quilljs/quill/issues/1089#issuecomment-318066471
const quill = quillRef.current.getEditor();
let fileInput = quill.root.querySelector("input.ql-image[type=file]");

// to prevent duplicate initialization I guess
if (fileInput === null) {
  fileInput = document.createElement("input");
  fileInput.setAttribute("type", "file");
  fileInput.setAttribute(
    "accept",
    "image/png, image/gif, image/jpeg, image/bmp, image/x-icon"
  );
  fileInput.classList.add("ql-image");

  fileInput.addEventListener("change", () => {
    const files = fileInput.files;
    const range = quill.getSelection(true);

    if (!files || !files.length) {
      console.log("No files selected");
      return;
    }

    const formData = new FormData();
    formData.append("file", files[0]);
    formData.append("uid", uid);
    formData.append("img_type", "detail");
    quill.enable(false);
    console.log(files[0]);
    axios
      .post("the/url/for/handle/uploading", formData)
      .then((response) => {
        // after uploading succeed add img tag in the editor.
        // for detail visit https://quilljs.com/docs/api/#editor
        quill.enable(true);
        quill.insertEmbed(range.index, "image", response.data.url);
        quill.setSelection(range.index + 1);
        fileInput.value = "";
      })
      .catch((error) => {
        console.log("quill image upload failed");
        console.log(error);
        quill.enable(true);
      });
  });
  quill.root.appendChild(fileInput);
}
fileInput.click();
  }

   I don't know much about useMemo
   but if i don't use the hook,
   the editor keeps rerendered resulting in losing focus and I guess perfomance trouble too.
  const modules = useMemo(
() => ({
  toolbar: {
    container: [
      [{ font: [] }],
      [{ size: ["small", false, "large", "huge"] }], // custom dropdown
      ["bold", "italic", "underline", "strike"], // toggled buttons

      [{ color: [] }, { background: [] }], // dropdown with defaults from theme
      [{ script: "sub" }, { script: "super" }], // superscript/subscript
      [{ header: 1 }, { header: 2 }], // custom button values
      ["blockquote", "code-block"],
      [{ list: "ordered" }, { list: "bullet" }],

      [{ indent: "-1" }, { indent: "+1" }], // outdent/indent
      [{ direction: "rtl" }], // text direction

      [{ align: [] }],
      ["link", "image"],
      ["clean"], // remove formatting button
    ],
    handlers: { image: imgHandler }, // Custom image handler
  },
}),
[]
  );

  return (
<ReactQuill
  forwardedRef={quillRef}
  modules={modules}
  value={value}
  onChange={onChange}
  {...props}
/>
  );
}
kartik tyagi
  • 6,256
  • 2
  • 14
  • 31
Amugae
  • 111
  • 1
  • 7
1

If you want to use ref in Next.js with dynamic import

you can use React.forwardRef API

more info

Palash Gupta
  • 390
  • 3
  • 10
1

In NextJS, React.useRef or React.createRef do not work with dynamic import.

You should Replace

const ReactQuill = dynamic(import('react-quill'), { ssr: false, loading: () => <p>Loading ...</p> }
);

with

import ReactQuill from 'react-quill';

and render after when window is loaded.

import ReactQuill from 'react-quill';
class Create extends Component {
    constructor() {
        super();
        this.reactQuillRef = React.createRef();
        this.state = {isWindowLoaded: false};
    }
    componentDidMount() {
        this.setState({...this.state, isWindowLoaded: true});
    }

    .........
    .........

   render(){
     return (
       <div>
         {this.isWindowLoaded && <ReactQuil {...this.props}/>}
       </div>
     )
   }

}
bill
  • 118
  • 5
0

Use onChange and pass all the arguments, here one example to use the editor.getHTML()


import React, { Component } from 'react'
import dynamic from 'next/dynamic'
import { render } from 'react-dom'

const QuillNoSSRWrapper = dynamic(import('react-quill'), {
  ssr: false,
  loading: () => <p>Loading ...</p>,
})

const modules = {
  toolbar: [
    [{ header: '1' }, { header: '2' }, { font: [] }],
    [{ size: [] }],
    ['bold', 'italic', 'underline', 'strike', 'blockquote'],
    [
      { list: 'ordered' },
      { list: 'bullet' },
      { indent: '-1' },
      { indent: '+1' },
    ],
    ['link', 'image', 'video'],
    ['clean'],
  ],
  clipboard: {
    // toggle to add extra line breaks when pasting HTML:
    matchVisual: false,
  },
}
/*
 * Quill editor formats
 * See https://quilljs.com/docs/formats/
 */
const formats = [
  'header',
  'font',
  'size',
  'bold',
  'italic',
  'underline',
  'strike',
  'blockquote',
  'list',
  'bullet',
  'indent',
  'link',
  'image',
  'video',
]

class BlogEditor extends Component {
  constructor(props) {
    super(props)
    this.state = { value: null } // You can also pass a Quill Delta here
    this.handleChange = this.handleChange.bind(this)
    this.editor = React.createRef()
  }

  handleChange = (content, delta, source, editor) => {
    this.setState({ value: editor.getHTML() })
  }

  render() {
    return (
      <>
        <div dangerouslySetInnerHTML={{ __html: this.state.value }} />
        <QuillNoSSRWrapper ref={this.editor} onChange={this.handleChange} modules={modules} formats={formats} theme="snow" />
        <QuillNoSSRWrapper value={this.state.value} modules={modules} formats={formats} theme="snow" />
      </>
    )
  }
}
export default BlogEditor