Definition
You've probably experienced typing "안녕" in Korean and, before the composition finishes — while it's still in a state like "ㅇ ㅏ ㄴ" — the screen re-renders once and the character breaks or gets typed twice. This concept is for when you want to prevent that breakage in your own editor.
The composition of an IME (Input Method Editor — an input tool that combines multiple keystrokes into one character) is a native composition session (a "temporary input session" before the character is committed) that the browser/OS maintains until multiple keystrokes are committed into a single character, as in Korean, Japanese, or Chinese. In a contenteditable-based (an HTML element made so the user can edit text directly) rich-text editor, if a DOM swap, React re-render, or serialize commit (the process of saving the editor content as a string/data) cuts in during composition, the session breaks and the character being composed gets duplicated, lost, or prematurely committed.
Why it matters
Web editors have a fast "keystroke → state update → DOM reflection" loop. The IME must maintain an "as-yet-uncommitted string" in the middle of that loop. If, during composition, the parent overwrites the guest value via prop, or a blur/unmount path calls commit, the browser's underlines and composition buffer reset. Before you design new custom compositionstart/end events, it is cost-effective to check whether the editor you're using already provides a composing flag.
How it works
- When the user starts input via IME, the browser opens a composition session (showing an underline).
- The
beforeinput/inputevents arrive differently in the during-composition vs. after-composition phases. - The editor judges "no flush right now" from a signal like
view.composing(ProseMirror) ornativeEvent.isComposing. - When composition is committed via
compositionend, commit/serialize is safe at that point. - In the React guest pattern, use mount-once initial content so a parent re-render doesn't clobber the composing text.
| Phase | Risky action | Safe pattern |
|---|---|---|
| Composing | blur commit, plugin menu open | composing gate |
| Composing | value replace via parent prop | mount-once guest |
| Verification | jsdom keyboard only | CDP imeSetComposition |
Practical application
In Playwright (Chromium), reproduce the composition phases sequentially with the DevTools Protocol Input.imeSetComposition, and commit with Input.insertText. jsdom/happy-dom do not faithfully mimic the composition lifecycle, so they can hardly be the sole basis for an IME regression.
Trade-offs
- composing defer can delay structural changes during composition (split/merge) — mostly a few ms to a few hundred ms, which is UX-acceptable.
- mount-once guest conflicts with the "parent forcibly resets content" pattern — if a reset is needed, restart explicitly with a keyed remount.
- Composition timing differs between Safari/WebKit and Chromium and can vary by environment, so cross-browser E2E is safest.
When not to use
- When you've explicitly scoped a single-language (ASCII only) internal tool to not support IME at all (even so, the mobile OS keyboard can still take the IME path).
- When you add only composition events but leave the commit gate untouched — event handlers alone can hardly block the entire flush path.
Common mistakes
- "Unit test is green, so IME is OK" — jsdom's composition/coordinate APIs are incomplete.
- A slash menu/mention plugin calling setState on every
view.update— parent re-render even during composition. - Duplicating commit at every blur/unmount/keyboard delegate — missing the composing check.
Related concepts
- focused-guest-editor — guest commit boundaries / mount-once
- contenteditable-keyboard-history-delegation — key/history interaction during focus