Preserving caret position in React contenteditable
How to solve caret position jumping issues when using contenteditable in React due to re-renders
Problem
When using contentEditable elements in React, the caret (cursor) position jumps to the beginning of the element whenever the component re-renders.
Solution
1. Use Uncontrolled Component Pattern
Manage the DOM directly using refs and minimize state updates.
import React, { useRef } from 'react';
function ContentEditable() {
const contentRef = useRef(null);
const handleInput = (e) => {
// Update state only on blur
const content = e.currentTarget.textContent;
// Update state only when needed
};
return (
<div
contentEditable
ref={contentRef}
onInput={handleInput}
suppressContentEditableWarning
/>
);
}
2. Save and Restore Caret Position
Save the caret position before updates and restore it after.
import React, { useRef, useEffect } from 'react';
function ContentEditable({ value, onChange }) {
const editableRef = useRef(null);
const caretPositionRef = useRef(null);
const saveCaretPosition = () => {
const selection = window.getSelection();
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
caretPositionRef.current = {
startContainer: range.startContainer,
startOffset: range.startOffset,
endContainer: range.endContainer,
endOffset: range.endOffset
};
}
};
const restoreCaretPosition = () => {
if (!caretPositionRef.current) return;
const selection = window.getSelection();
const range = document.createRange();
range.setStart(
caretPositionRef.current.startContainer,
caretPositionRef.current.startOffset
);
range.setEnd(
caretPositionRef.current.endContainer,
caretPositionRef.current.endOffset
);
selection.removeAllRanges();
selection.addRange(range);
};
useEffect(() => {
if (editableRef.current && value !== editableRef.current.textContent) {
saveCaretPosition();
editableRef.current.textContent = value;
restoreCaretPosition();
}
}, [value]);
return (
<div
contentEditable
ref={editableRef}
onInput={(e) => {
saveCaretPosition();
onChange(e.currentTarget.textContent);
}}
suppressContentEditableWarning
/>
);
}
3. Use use-editable Library
Use a library that automatically handles caret management.
import { useEditable } from '@use-editable/core';
function ContentEditable({ value, onChange }) {
const { editableRef } = useEditable({
value,
onChange,
});
return (
<div
ref={editableRef}
contentEditable
suppressContentEditableWarning
/>
);
}
Notes
- This issue is more prevalent in Safari and Firefox, so test in these browsers
- Debounce or throttle state updates to reduce re-render frequency
- Use React.memo to prevent unnecessary re-renders