React中contenteditable ul列表的状态管理问题
解决可编辑
- 作为数组表单字段时的重复值问题
你遇到的核心问题是React受控组件逻辑与contentEditable元素的DOM自主更新机制冲突:每次onInput触发时,你从innerHTML解析出数组并更新状态,React随即重新渲染<ul>的子元素,把新的<li>插入到已被用户编辑过的DOM中,导致DOM内容重复,下次解析时就会生成重复的数组项,形成恶性循环。
方案1:优化临时方案(onBlur触发同步)
既然你已经发现onBlur能避免重复问题,可直接完善这个方案——仅在用户编辑完成、失去焦点时同步状态,避免频繁渲染导致的DOM冲突:
import { useState, type FormEvent } from 'react'; export default function List() { const [value, setValue] = useState<string[]>(['first line', 'second line']); function handleBlur(event: FormEvent) { const items = event.currentTarget.innerHTML .split(/<li>(.*?)<\/li>/) ?.map((item: string) => item.replace(/<(.*?)>/g, '')) ?.filter(Boolean); setValue(items); } return ( <ul contentEditable suppressContentEditableWarning onBlur={handleBlur} > {value.map((item, index) => ( <li key={index}>{item}</li> ))} </ul> ); }
这个方案逻辑简单,完全复用React的状态管理,适合对实时同步要求不高的场景。
方案2:实时同步且避免重复(直接操作DOM)
如果需要实时同步状态,可通过useRef直接操作<ul>的DOM元素,绕过React对子元素的渲染控制,避免重复渲染冲突:
import { useState, useRef, useEffect, type FormEvent } from 'react'; export default function List() { const [value, setValue] = useState<string[]>(['first line', 'second line']); const ulRef = useRef<HTMLUListElement>(null); // 实时解析DOM中的<li>文本同步到状态 function handleInput() { if (!ulRef.current) return; const items = Array.from(ulRef.current.children) .map(li => (li as HTMLLIElement).textContent?.trim() || '') .filter(Boolean); setValue(items); } // 当外部状态更新时,同步到DOM(避免状态与DOM不一致) useEffect(() => { if (!ulRef.current) return; // 仅当状态与当前DOM内容不一致时才更新,避免不必要的DOM操作 const currentDomItems = Array.from(ulRef.current.children) .map(li => (li as HTMLLIElement).textContent?.trim() || '') .filter(Boolean); if (JSON.stringify(currentDomItems) !== JSON.stringify(value)) { ulRef.current.innerHTML = value.map(item => `<li>${item}</li>`).join(''); } }, [value]); return ( <ul ref={ulRef} contentEditable suppressContentEditableWarning onInput={handleInput} /> ); }
关键逻辑说明:
- 用
useRef获取<ul>的DOM引用,直接从DOM中遍历<li>获取文本,解析逻辑更准确,无需处理HTML标签拆分的问题。 - 通过
useEffect在状态变化时同步DOM内容,且增加了一致性判断,避免重复更新DOM。
关于光标跳转问题的补充
你提到的dangerouslySetInnerHTML导致光标跳转的问题,可通过Selection API解决:在更新DOM前保存当前光标位置,更新后恢复。示例代码片段:
// 保存光标位置 function saveSelection() { const selection = window.getSelection(); if (!selection || selection.rangeCount === 0) return null; const range = selection.getRangeAt(0); const preSelectionRange = range.cloneRange(); preSelectionRange.selectNodeContents(ulRef.current!); preSelectionRange.setEnd(range.startContainer, range.startOffset); const start = preSelectionRange.toString().length; return { start, end: start + range.toString().length }; } // 恢复光标位置 function restoreSelection(positions: { start: number; end: number }) { const selection = window.getSelection(); const range = document.createRange(); range.setStart(ulRef.current!, 0); range.collapse(true); const textNode = ulRef.current!.firstChild; if (textNode) { range.setStart(textNode, positions.start); range.setEnd(textNode, positions.end); selection?.removeAllRanges(); selection?.addRange(range); } } // 在useEffect中使用 useEffect(() => { if (!ulRef.current) return; const currentDomItems = Array.from(ulRef.current.children) .map(li => (li as HTMLLIElement).textContent?.trim() || '') .filter(Boolean); if (JSON.stringify(currentDomItems) !== JSON.stringify(value)) { const selection = saveSelection(); ulRef.current.innerHTML = value.map(item => `<li>${item}</li>`).join(''); if (selection) restoreSelection(selection); } }, [value]);
这个逻辑可以在更新DOM后恢复光标到之前的位置,提升编辑体验。
内容的提问来源于stack exchange,提问作者armageddon
相关产品推荐
相关产品推荐

