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

Ant Design Form.List 中使用 TipTap 富文本编辑器时,文本格式化触发全表单重校验的问题

Ant Design Form.List 中使用 TipTap 富文本编辑器时,文本格式化触发全表单重校验的问题

嘿,我猜你现在头疼的点是:每次在 TipTap 里点加粗、斜体这类格式化按钮,整个表单的校验就会自动跑一遍,对吧?这确实挺影响体验的,我来帮你拆解问题根源,再给你落地的解决方案。


先理清楚你的核心实现

1. Form.List 嵌入富文本编辑器的代码

<Form.List name="tableData">
  {(fields, { add, remove }) => (
    <Table
      dataSource={fields}
      columns={[
        {
          title: "Nội dung mục tiêu",
          render: (_, __, index) => (
            <Form.Item
              name={[fields[index].name, "content"]}
              rules={[{ required: true, message: "Vui lòng nhập nội dung" }]}
              validateTrigger="onBlur"
              getValueFromEvent={(val) => val}
            >
              <RichTextEditor placeholder="Nhập nội dung..." />
            </Form.Item>
          ),
        },
      ]}
    />
  )}
</Form.List>

2. RichTextEditor 核心实现

import { useEditor, EditorContent } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import Placeholder from "@tiptap/extension-placeholder";
import Underline from "@tiptap/extension-underline";
import TextAlign from "@tiptap/extension-text-align";
import Link from "@tiptap/extension-link";
import Image from "@tiptap/extension-image";
import { useEffect, useRef } from "react";
import {
  BoldOutlined, ItalicOutlined, UnderlineOutlined,
  OrderedListOutlined, UnorderedListOutlined,
  AlignLeftOutlined, AlignCenterOutlined, AlignRightOutlined,
  LinkOutlined, PictureOutlined
} from "@ant-design/icons";

// 工具栏组件
const MenuBar = ({ editor, disabled, formatHandler }) => {
  if (!editor) return null;

  const insertLink = () => {
    const url = window.prompt("Nhập URL:");
    if (url) editor.chain().focus().setLink({ href: url }).run();
  };

  const insertImage = () => {
    const url = window.prompt("Nhập URL ảnh:");
    if (url) editor.chain().focus().setImage({ src: url }).run();
  };

  return (
    <div className="tiptap-toolbar" style={{ backgroundColor: disabled ? "#e0e0e0" : "#ffffff", borderRadius: "8px", padding: "5px" }} >
      <button onClick={() => formatHandler(() => editor.chain().focus().toggleBold().run())} disabled={disabled} className={editor.isActive("bold") ? "active" : ""} >
        <BoldOutlined />
      </button>
      <button onClick={() => formatHandler(() => editor.chain().focus().toggleItalic().run())} disabled={disabled} className={editor.isActive("italic") ? "active" : ""} >
        <ItalicOutlined />
      </button>
      {/* 其他格式化按钮省略 */}
    </div>
  );
};

// 主编辑器组件
const RichTextEditor = ({ value = "", onChange, onBlur, onTouched, disabled = false, placeholder = "Nhập nội dung...", style = {} }) => {
  const lastHtmlRef = useRef(value);
  const isFormattingRef = useRef(false);

  const formatHandler = (command) => {
    isFormattingRef.current = true;
    command();
    setTimeout(() => isFormattingRef.current = false, 100);
  };

  const editor = useEditor({
    extensions: [
      StarterKit,
      Placeholder({ placeholder }),
      Underline,
      TextAlign.configure({ types: ["heading", "paragraph"] }),
      Link,
      Image,
    ],
    content: value,
    disabled,
    onUpdate: ({ editor }) => {
      const html = editor.getHTML();
      if (html !== lastHtmlRef.current && !isFormattingRef.current) {
        onChange?.(html);
        lastHtmlRef.current = html;
      }
    },
  });

  useEffect(() => {
    if (!editor) return;
    const handleBlur = () => {
      onBlur?.();
      onTouched?.();
    };
    editor.on("blur", handleBlur);
    return () => editor.off("blur", handleBlur);
  }, [editor, onBlur, onTouched]);

  return (
    <div style={style}>
      <MenuBar editor={editor} disabled={disabled} formatHandler={formatHandler} />
      <EditorContent editor={editor} />
    </div>
  );
};

问题根源分析

你已经用isFormattingRef试图区分格式化操作,但还是出问题,主要有两个核心原因:

  1. TipTap的onUpdate触发太频繁:格式化操作(比如加粗)会改变编辑器的HTML内容,即使你标记了isFormattingRef.current = true,setTimeout的100ms延迟可能赶不上onUpdate的触发时机,导致还是会触发onChange。
  2. Form.Item的隐式更新逻辑:即使你设置了validateTrigger="onBlur",AntD Form在字段值变化时,还是可能触发隐式的校验逻辑,尤其是在Form.List的嵌套结构下,单个字段的变化可能会触发整个列表的重渲染和校验。

解决方案(最优组合)

1. 优化RichTextEditor:彻底区分格式化与内容输入

修改编辑器的onUpdate和formatHandler逻辑,确保只有用户输入内容(非格式化操作)或编辑器失焦时,才触发onChange:

const RichTextEditor = ({ value = "", onChange, onBlur, onTouched, disabled = false, placeholder = "Nhập nội dung...", style = {} }) => {
  const lastHtmlRef = useRef(value);
  const isFormattingRef = useRef(false);
  const prevContentRef = useRef(value);

  const formatHandler = (command) => {
    isFormattingRef.current = true;
    prevContentRef.current = editor?.getHTML() || value;
    command();
    // 格式化是同步操作,立即标记为非格式化状态
    setTimeout(() => {
      isFormattingRef.current = false;
      prevContentRef.current = editor?.getHTML() || value;
    }, 0);
  };

  const editor = useEditor({
    extensions: [
      StarterKit,
      Placeholder({ placeholder }),
      Underline,
      TextAlign.configure({ types: ["heading", "paragraph"] }),
      Link,
      Image,
    ],
    content: value,
    disabled,
    onUpdate: ({ editor }) => {
      const currentHtml = editor.getHTML();
      // 只有非格式化操作 + 内容真的变化时,才触发onChange
      if (!isFormattingRef.current && currentHtml !== prevContentRef.current) {
        onChange?.(currentHtml);
        prevContentRef.current = currentHtml;
        lastHtmlRef.current = currentHtml;
      }
    },
  });

  // 失焦时强制同步内容并触发校验
  useEffect(() => {
    if (!editor) return;
    const handleBlur = () => {
      const currentHtml = editor.getHTML();
      if (currentHtml !== lastHtmlRef.current) {
        onChange?.(currentHtml);
        lastHtmlRef.current = currentHtml;
      }
      onBlur?.();
      onTouched?.();
    };
    editor.on("blur", handleBlur);
    return () => editor.off("blur", handleBlur);
  }, [editor, onBlur, onTouched, onChange]);

  // 监听外部value变化(比如表单重置)
  useEffect(() => {
    if (editor && value !== prevContentRef.current) {
      prevContentRef.current = value;
      lastHtmlRef.current = value;
      editor.commands.setContent(value);
    }
  }, [editor, value]);

  return (
    <div style={style}>
      <MenuBar editor={editor} disabled={disabled} formatHandler={formatHandler} />
      <EditorContent editor={editor} />
    </div>
  );
};

2. 优化Form.Item:精确控制更新与校验时机

修改Form.Item的配置,确保只有必要时才更新字段和触发校验:

<Form.Item
  name={[fields[index].name, "content"]}
  rules={[{ required: true, message: "Vui lòng nhập nội dung" }]}
  // 明确只在失焦时触发校验
  validateTrigger={["onBlur"]}
  getValueFromEvent={(val) => val}
  // 精确控制Form.Item何时更新,避免不必要的重渲染
  shouldUpdate={(prevValues, currentValues) => {
    const prevVal = prevValues.tableData[index]?.content;
    const currentVal = currentValues.tableData[index]?.content;
    return prevVal !== currentVal;
  }}
>
  <RichTextEditor placeholder="Nhập nội dung..." />
</Form.Item>

额外提示

  • 如果还是有问题,可以在Form组件层面设置validateTrigger="onBlur",统一控制整个表单的校验触发时机。
  • 提交表单时,记得手动调用form.validateFields()来做最终校验,确保数据合法。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.08 14:23:05