์ผ€์ด์Šค ce-0316-react-caret-jumps-on-rerender-ko ยท ์‹œ๋‚˜๋ฆฌ์˜ค scenario-react-caret-jumps-on-rerender

Caret position jumps to beginning on React re-render

OS: Any Any ๊ธฐ๊ธฐ: Desktop or Laptop Any ๋ธŒ๋ผ์šฐ์ €: Safari Latest ํ‚ค๋ณด๋“œ: US ์ดˆ์•ˆ
reactcaretrerendersafarifirefox

Phenomenon

When using contentEditable elements in React, the caret (cursor) jumps to the beginning of the element upon re-rendering. This behavior is particularly noticeable in Safari and Firefox. The root cause is that Reactโ€™s re-rendering process replaces the DOM node, causing the browser to reset the caret position.

Reproduction example

  1. Create a React component with a contentEditable div controlled by state.
  2. Type some text and place cursor in the middle.
  3. Trigger a state update that causes re-render.
  4. Observe that caret position jumps to the beginning.

Observed behavior

  • Caret jumps: Caret position reverts to start of element on re-render.
  • Safari/Firefox: More prevalent in Safari and Firefox.
  • DOM replacement: React replaces DOM nodes during re-render, losing caret position.
  • State updates: Any state change that triggers re-render causes the issue.
  • User experience: Disrupts typing flow and editing experience.

Expected behavior

  • Caret position should be preserved during re-renders.
  • DOM updates should not reset cursor position.
  • Editing experience should remain smooth during state updates.

Analysis

Reactโ€™s reconciliation algorithm may replace DOM nodes when state changes, causing the browser to lose track of the caret position. Safari and Firefox handle DOM updates differently from Chrome, making them more susceptible to this issue.

Workarounds

  • Use uncontrolled components with refs:
    const contentRef = useRef(null);
    return <div contentEditable ref={contentRef} onInput={handleInput} />;
  • Preserve and restore caret position manually:
    useEffect(() => {
      const el = contentRef.current;
      const range = document.createRange();
      const sel = window.getSelection();
      range.setStart(el.childNodes[0], savedPosition);
      range.collapse(true);
      sel.removeAllRanges();
      sel.addRange(range);
    }, [content]);
  • Use libraries like use-editable hook that handle caret management.
  • Avoid controlled contentEditable when possible, use uncontrolled approach.

Playground for this case

Use the reported environment as a reference and record what happens in your environment while interacting with the editable area.

Reported environment
OS: Any Any
Device: Desktop or Laptop Any
Browser: Safari Latest
Keyboard: US
Your environment
Sample HTML:
Event log
Use this log together with the case description when filing or updating an issue.
0 events
Interact with the editable area to see events here.