
使用 dangerouslySetInnerHTML 更新 contentEditable 内容时,频繁重置 innerHTML 会导致光标跳回开头;通过 useRef 直接操作 DOM 节点并延迟同步状态,可有效保持续写光标位置。
使用 `dangerouslysetinnerhtml` 更新 `contenteditable` 内容时,频繁重置 innerhtml 会导致光标跳回开头;通过 `useref` 直接操作 dom 节点并延迟同步状态,可有效保持续写光标位置。
在构建基于 contentEditable 的自定义富文本编辑器时,一个高频痛点是:用户每次输入后,光标自动跳转至编辑区域起始位置。根本原因在于:React 中 dangerouslySetInnerHTML 是“一次性快照式”渲染机制——每当 content 状态更新并触发重新渲染时,React 会销毁并重建整个 div 的 DOM 子树,导致浏览器原生光标位置信息丢失。
上述问题代码中,onInput 直接调用 setContent(...) 触发状态更新,进而引发组件重渲染 + dangerouslySetInnerHTML 重载,光标必然重置。解决方案的核心是 分离 DOM 操作与状态同步时机:
✅ 正确做法:使用 useRef 获取对 contentEditable 容器的直接引用,在 onInput 中仅读取并清洗当前 HTML(不立即更新 state),再手动同步 content 状态——避免因状态变更触发不必要的 DOM 重建。
React 与 Next.js 性能优化指南,源自 Vercel 工程团队。适用于编写、审查或重构 React/Next.js 代码时使用。
以下是优化后的完整实现:
import React, { useState, useRef, useEffect } from 'react';
function App() {
const [content, setContent] = useState('');
const editableRef = useRef(null);
const freeAllAttributeHTML = (htmlString) => {
// 使用 replaceAll 替代正则全局匹配(注意:需确保环境支持)
let cleaned = String(htmlString)
.replaceAll(/<div[^>]*>/g, '<div>')
.replaceAll(/<p[^>]*>/g, '<p>')
.replaceAll(/<span[^>]*>/g, '<span>')
.replaceAll(/<button[^>]*>/g, '<button>')
.replaceAll(/<a[^>]*>/g, '<a>')
.replaceAll(/<svg[^>]*>/g, '<svg>')
.replaceAll(/<img[^>]*>/g, ''); // 移除 img 标签(含所有属性)
return cleaned;
};
const handleContentChange = () => {
if (!editableRef.current) return;
const rawHTML = editableRef.current.innerHTML;
const cleaned = freeAllAttributeHTML(rawHTML);
setContent(cleaned);
};
// ⚠️ 关键补充:防止初始渲染时 ref 为空导致异常
useEffect(() => {
if (editableRef.current && !content) {
editableRef.current.innerHTML = '';
}
}, [content]);
return (
<>
<div>
<strong>hello one</strong>
<em> hello two</em>
</div>
<div
className="editable"
ref={editableRef}
contentEditable={true}
dangerouslySetInnerHTML={{ __html: content }}
onInput={handleContentChange}
// 可选:添加样式确保可编辑体验
style={{
minHeight: '100px',
border: '1px solid #ccc',
padding: '8px',
outline: 'none'
}}
/>
</>
);
}
export default App;? 关键要点说明:
- useRef 提供对 DOM 节点的稳定引用,绕过 React 渲染周期,使 innerHTML 读取不依赖状态更新;
- onInput 中仅执行清洗与 setContent,不修改 DOM,因此光标位置由浏览器原生维护;
- 避免在 dangerouslySetInnerHTML 同步过程中混入 setState —— 否则会形成「读 DOM → 改 state → 重渲染 → 清空 DOM → 重载 HTML」的破坏性循环;
- 如需更健壮的 HTML 清洗,建议使用 DOMPurify.sanitize() 替代手工正则(防范 XSS 并提升兼容性);
- 生产环境推荐配合 useCallback 缓存 handleContentChange,并考虑节流防抖以减少高频输入下的状态更新压力。
该方案在保持轻量级的同时,精准解决了 contentEditable 光标丢失问题,为自研富文本编辑器奠定了可靠基础。

















