You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React排序笔记时TinyMCE编辑器全量重渲染问题求助

问题描述

我正在开发一款工地记录笔记并生成PDF报告的工具,目前笔记的增删改功能均正常,但使用react-sortable-hoc对笔记集合进行拖动排序(灰色按钮为拖拽手柄)时,所有TinyMCE编辑器都会被重新渲染——实际上仅需更新笔记位置,编辑器内容并未改变,却触发了全量初始化。这一操作耗时过长,且若处于页面底部,编辑器的重新初始化会导致页面回滚至顶部。请问是否有办法仅更新编辑器位置而不重新初始化所有编辑器?


关键代码片段

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>
    )
  );
}

解决方案

问题核心是React在排序时错误地销毁并重建了所有TinyMCE组件,主要原因是key值不稳定、直接修改原数组和组件未做重渲染优化,以下是针对性修复:

1. 修复组件key值,确保唯一且稳定

当前SortableNote的key使用note.id,但新建笔记没有id,会导致key重复或变化,React无法识别组件身份,排序时会销毁重建所有组件。改成用每个笔记都有的uuid:

// RapportChantier.jsx 中SortableNote的key修改
<SortableNote
  key={note.uuid}
  note={note}
  pushNote={pushNote}
  removeNote={removeNote}
  index={index}
/>

2. 使用不可变数组操作,避免直接修改原数组

直接修改notes数组再调用setNotes,React无法检测到数组变化,会触发不必要的全量重渲染,改成创建新数组:

// RapportChantier.jsx 中addNote修改
const addNote = () => {
  const newNote = {
    uuid: uuidv4(),
    id: null,
    content: "Ecrivez votre note ici ...",
    position: 0,
    rapport: "/api/rapport_chantiers/" + rapport.id,
  };
  setNotes([...notes, newNote]);
};

// RapportChantier.jsx 中pushNote修改
const pushNote = (pushedNote) => {
  setNotes(notes.map(note => 
    note.uuid === pushedNote.uuid ? pushedNote : note
  ));
};

3. 用React.memo优化Note组件,减少不必要重渲染

给Note组件包裹React.memo,指定自定义比较函数,只有当关键props变化时才重渲染:

// Notes.jsx 修改
import React, { memo } from 'react';

export const SortableNote = SortableElement(Note);

export default memo(function Note({ note: noteProps, removeNote, pushNote }) {
  // 原有组件逻辑不变
}, (prevProps, nextProps) => {
  // 仅当笔记的核心属性或回调函数变化时才重渲染
  return prevProps.note.uuid === nextProps.note.uuid &&
         prevProps.note.content === nextProps.note.content &&
         prevProps.note.id === nextProps.note.id &&
         prevProps.removeNote === nextProps.removeNote &&
         prevProps.pushNote === nextProps.pushNote;
});

4. 优化SortableContainer的props传递

将notes、pushNote、removeNote作为props传给SortableNotes,避免内部直接依赖外部state,让React更精准地判断组件更新:

// RapportChantier.jsx 中SortableNotes修改
const SortableNotes = SortableContainer(({ notes, pushNote, removeNote, className }) => {
  return (
    <div className={className}>
      {notes.map((note, index) => (
        <SortableNote
          key={note.uuid}
          note={note}
          pushNote={pushNote}
          removeNote={removeNote}
          index={index}
        />
      ))}
    </div>
  );
});

// return中的SortableNotes修改
<SortableNotes
  className="mb-3"
  notes={notes}
  pushNote={pushNote}
  removeNote={removeNote}
  axis="y"
  onSortEnd={onSortEnd}
  useDragHandle={true}
  lockAxis="y"
/>

5. 同步Note组件内部状态与props

在Note组件中添加useEffect监听noteProps变化,确保内部状态与外部props同步:

// Notes.jsx 中添加
useEffect(() => {
  setNote(noteProps);
  setContent(noteProps.content);
}, [noteProps]);

内容的提问来源于stack exchange,提问作者Jayss230

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.13 05:20:28