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试图区分格式化操作,但还是出问题,主要有两个核心原因:
- TipTap的
onUpdate触发太频繁:格式化操作(比如加粗)会改变编辑器的HTML内容,即使你标记了isFormattingRef.current = true,setTimeout的100ms延迟可能赶不上onUpdate的触发时机,导致还是会触发onChange。 - 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




