🎯 Mastering Focus Management in React: flushSync, Inline Editing, and Accessibility
When building interactive UIs, especially with forms and dynamic elements, managing focus properly becomes critical. It’s not just about making things “work”—it’s about accessibility, keyboard navigation, and creating a smooth experience for everyone.
Let’s walk through common focus pitfalls and learn how to solve them using flushSync, ref, and smart event handling with a practical real-world example.
Good focus management ensures that:
Consider this component:
function MyComponent() {const [show, setShow] = useState(false)return (<div><button onClick={() => setShow(true)}>Show</button>{show ? <input /> : null}</div>)}
When the user clicks the Show button, the input appears. Naturally, you’d expect the input to be focused so the user can type immediately. But…
const inputRef = useRef<HTMLInputElement>(null)<buttononClick={() => {setShow(true)inputRef.current?.focus() // ❌ Doesn't work}}>Show</button>
This doesn’t work because React batches state updates. The DOM isn’t updated when setShow(true) is called, so the input doesn’t yet exist.
flushSyncTo handle this properly, we can use flushSync from react-dom to flush the state update synchronously, ensuring the DOM updates before we call focus().
import { flushSync } from 'react-dom'function MyComponent() {const inputRef = useRef<HTMLInputElement>(null)const [show, setShow] = useState(false)return (<div><buttononClick={() => {flushSync(() => {setShow(true)})inputRef.current?.focus()}}>Show</button>{show ? <input ref={inputRef} /> : null}</div>)}
💡 Use
flushSyncsparingly. It’s a performance de-optimization, but it’s perfect for situations like managing focus where user experience matters most.
Let’s now build an <EditableText /> component that:
Enter, or Escape, exits edit mode and goes back to the buttonWhen the user clicks the button, it disappears and is replaced with the input. If we don’t manage focus, the user is left staring at a blank screen with no cursor in sight.
import { flushSync } from 'react-dom'import React, { useRef, useState } from 'react'function EditableText() {const [isEditing, setIsEditing] = useState(false)const [value, setValue] = useState('Click to edit me')const inputRef = useRef<HTMLInputElement>(null)const startEditing = () => {flushSync(() => setIsEditing(true))requestAnimationFrame(() => {inputRef.current?.focus()inputRef.current?.select()})}const stopEditing = (submit: boolean) => {setIsEditing(false)if (submit && inputRef.current) {setValue(inputRef.current.value)}}return (<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}><button>Before</button>{isEditing ? (<inputref={inputRef}defaultValue={value}onBlur={() => stopEditing(true)}onKeyDown={(e) => {if (e.key === 'Enter') stopEditing(true)if (e.key === 'Escape') stopEditing(false)}}/>) : (<button onClick={startEditing}>{value}</button>)}<button>After</button></div>)}
flushSync + requestAnimationFrame)Enter submits, Escape cancelsThis solution improves usability and accessibility:
ref + focus() to control where focus goesflushSync() when you need DOM to update synchronouslyBy thoughtfully managing focus, you’re not just fixing bugs—you’re creating a smooth, delightful, and accessible experience for every user.
Happy coding! 🚀