🎯 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.
flushSync
To 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
flushSync
sparingly. 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! 🚀
Quick Links
Legal Stuff
Social Media