reactjstinymcereact-sortable-hoc

Re-render issue when trying to make a sortable collection of notes with react, tinymce and react-sortable-hoc


I am developing a tool to take some notes on worksites to generate a pdf report.

A this stage, everthing is working properly (add, edit, remove a note ) but when I reorder the notes collection (the drag handle is the grey button ) , React re-render all TinyMCE editors of the collection whereas it should only update notes positions. TinyMCE editors content have not changed, only positions have been updated. See above onSortEnd() function.

This operation takes too long. In addition, if I am at the bottom of the page the re-initialization of all TinyMCE editors making the page back to top.

So my question is :

Is there a way to update editors position without re-init all of them ?

Find attached a screen of my interface.

I'm french, sorry for my bad english.

enter image description here

Here extracts of important code :

RapportChantier.jsx

export function AddEdit() {

  const [rapport, setRapport] = useState({ id: null });
  const [notes, setNotes] = useState([]);
  

  const addNote = () => {
    notes.push({
      uuid: uuidv4(),
      id: null,
      content: "Ecrivez votre note ici ...",
      position: 0,
      rapport: "/api/rapport_chantiers/" + rapport.id,
    });
    setNotes(notes);
  };

  const removeNote = (id) => {
    const filteredNotes = notes.filter((note) => {
      if (note.id !== null) {
        return note.id !== id;
      } else {
        return note.uuid !== id;
      }
    });
    setNotes(filteredNotes);
  };

  const pushNote = (pushedNote) => {
    const pushedNoteIndex = notes.findIndex(
      (note) => note.uuid === pushedNote.uuid
    );
    notes[pushedNoteIndex] = pushedNote;
    setNotes(notes);
  };

  const onSortEnd = ({ oldIndex, newIndex }) => {
    const reorderedNotes = arrayMove(notes, oldIndex, newIndex);
    setNotes(reorderedNotes);
  };

  const SortableNotes = SortableContainer((props) => {
    return (
      <div className={props.className}>
        {notes.map((note, index) => (
          <SortableNote
            key={note.id}
            note={note}
            pushNote={pushNote}
            removeNote={removeNote}
            index={index}
          />
        ))}
      </div>
    );
  });

  return (
    <div className="bg-white" style={{ padding: "30px" }}>

      <SortableNotes
        className="mb-3"
        axis="y"
        onSortEnd={onSortEnd}
        useDragHandle={true}
        lockAxis={"y"}
      />

      <button onClick={addNote} className="btn btn-primary btn-sm">
        Ajouter une note
      </button>
    </div>
  );
}

Notes.jsx

export const SortableNote = SortableElement(Note);

export default function Note({ note: noteProps, removeNote, pushNote }) {
  const editorRef = useRef(null);

  const [note, setNote] = useState({ ...noteProps });
  const [content, setContent] = useState(noteProps.content);
  const [timeoutUpdate, setTimeoutUpdate] = useState(null);
  const [hasChanged, setHasChanged] = useState(0);

  const updateNote = async () => {
    if (!note.id) {
      const { status, response } = await sendJsonData(
        "/api/notes",
        { ...note, content },
        "post"
      );

      if (status === 201) {
        setNote(response);
      }
    } else {
      const { status, response } = await sendJsonData(
        "/api/notes/" + note.id,
        { ...note, content },
        "patch"
      );
      if (status === 200) {
        setNote(response);
      }
    }
  };

  useEffect(() => {
    if (hasChanged > 0) {
      clearTimeout(timeoutUpdate);
      setTimeoutUpdate(setTimeout(updateNote, 1000));
    }
  }, [hasChanged, content]);

  const DragHandle = SortableHandle(() => (
    <button
      className="btn btn-secondary btn-sm"
      onClick={(e) => e.preventDefault()}
    >
      <span className="icon fa-reorder"></span>
    </button>
  ));

  const handleChanges = (editorContent) => {
    setContent(editorContent);
    setHasChanged(hasChanged + 1);
  };

  return (
    note && (
      <div className="mt-3">
        <div className="row">
          <div className="col-11">
            <Editor
              tinymceScriptSrc={"/libs/tinymce/tinymce.min.js"}
              value={content ?? ""}
              onEditorChange={handleChanges}
              onInit={(evt, editor) => (editorRef.current = editor)}
              init={{
                height: 300,
                menubar: false,
                plugins: [],
                toolbar:
                  "undo redo | blocks | bold italic strikethrough underline forecolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | removeformat",

                content_style:
                  "body { font-family:Helvetica,Arial,sans-serif; font-size:14px }",
              }}
            />
          </div>

          <div className="col-1">
            <button className="btn btn-primary btn-sm">
              <span className="icon fa-plus"></span>
            </button>
            <DragHandle />
            <button
              className="btn btn-danger btn-sm"
              onClick={(e) => {
                e.preventDefault();
                removeNote(note.id ?? note.uuid);
              }}
            >
              <span className="icon fa-remove"></span>
            </button>
          </div>
        </div>
      </div>
    )
  );
}

Solution

  • I've found a solution to my problem.

    The probleme was that react-sortable-hoc lib does not keep the node ref of sortables items.

    So after an item is dragged and dropped, all sortables items are removed from the collection and recreated.

    Between this moment (when items are removed and recreated), all TinyMCE editor are reinitialized and if the height of the page content is higher than the window height, the page will automatically back to top.

    I've solved this problem by using a more recent package @dnd-kit/sortable which allow you to get the node ref of sortable item and set it in a <div> or <whatever> :

    You can see an example of use here: https://codesandbox.io/s/x9w71?file=/src/App.js