One React mistake that's slowing you down
Simplify and speed up your app development using React composition

Focus management is one of those things you don't think about until it's broken.
But when it is, it can make your app feel clunky, inaccessible, or just plain
wrong. Let's talk about a little-known React API that can help you nail focus
management: flushSync
.
React is fast and smart about how it updates the DOM. When you call a state
updater like setShow(true)
, React doesn't immediately re-render your
component. Instead, it batches up state updates and processes them all at once,
after your event handler finishes. This is great for performance, but it can
trip you up when you need to interact with the DOM right after a state change.
Here's a classic example:
function MyComponent() { const [show, setShow] = useState(false) return ( <div> <button onClick={() => setShow(true)}>Show</button> {show ? <input /> : null} </div> )}
Suppose you want to focus the input as soon as it appears. You might try this:
function MyComponent() { const inputRef = useRef<HTMLInputElement>(null) const [show, setShow] = useState(false) return ( <div> <button onClick={() => { setShow(true) inputRef.current?.focus() // This probably won't work! }} > Show </button> {show ? <input ref={inputRef} /> : null} </div> )}
But this doesn't work! Why? Because when you call setShow(true)
, React
schedules the update, but doesn't apply it until after your handler finishes.
So when you try to focus the input, it doesn't exist in the DOM yet.
Early in my career, I tried to work around this by wrapping my focus call in a
setTimeout
or requestAnimationFrame
. Sometimes it worked, sometimes it
didn't. The timing depended on the device, the browser, and what else was
happening in the app. It was unreliable and hacky.
onClick={() => { setShow(true) setTimeout(() => { inputRef.current?.focus() }, 10) // 🤞}}
But this is a guessing game. You don't want to rely on magic numbers or hope that the browser is fast enough. You want a guarantee.
flushSync
flushSync
from react-dom
is your escape hatch. It tells React: "Hey, I know you like to batch updates for
performance, but I need you to process this update right now." Any state
updates inside the flushSync
callback are applied immediately, so the DOM is
up-to-date as soon as the callback finishes.
Here's how you'd use it:
import { flushSync } from 'react-dom'function MyComponent() { const inputRef = useRef<HTMLInputElement>(null) const [show, setShow] = useState(false) return ( <div> <button onClick={() => { flushSync(() => { setShow(true) }) inputRef.current?.focus() }} > Show </button> {show ? <input ref={inputRef} /> : null} </div> )}
Now, when you click the button, the input appears and is immediately focused. No hacks, no guessing, just reliable focus management.
Let's look at a more practical example from the
Epic React Advanced React APIs workshop.
This specific component was borrowed from
Ryan Florence (thanks Ryan!). We have an
<EditableText />
component that lets users edit a piece of text inline. When
the user clicks the button, it turns into an input. When they submit, blur, or
hit escape, it turns back into a button. We want to:
Here's how we do it:
import { useRef, useState } from 'react'import { flushSync } from 'react-dom'function EditableText({ initialValue = '', fieldName, inputLabel, buttonLabel,}: { initialValue?: string fieldName: string inputLabel: string buttonLabel: string}) { const [edit, setEdit] = useState(false) const [value, setValue] = useState(initialValue) const inputRef = useRef<HTMLInputElement>(null) const buttonRef = useRef<HTMLButtonElement>(null) return edit ? ( <form onSubmit={(event) => { event.preventDefault() flushSync(() => { setValue(inputRef.current?.value ?? '') setEdit(false) }) buttonRef.current?.focus() }} > <input required ref={inputRef} type="text" aria-label={inputLabel} name={fieldName} defaultValue={value} onKeyDown={(event) => { if (event.key === 'Escape') { flushSync(() => { setEdit(false) }) buttonRef.current?.focus() } }} onBlur={(event) => { flushSync(() => { setValue(event.currentTarget.value) setEdit(false) }) buttonRef.current?.focus() }} /> </form> ) : ( <button aria-label={buttonLabel} ref={buttonRef} type="button" onClick={() => { flushSync(() => { setEdit(true) }) inputRef.current?.select() }} > {value || 'Edit'} </button> )}
This approach ensures that focus is always exactly where the user expects it to be, no matter how quickly they interact with the UI.
Why go to all this trouble? Because keyboard accessibility matters for everyone. Some users rely on the keyboard due to disabilities, but many power users (like me and probably you!) just prefer it. If your focus management is broken, keyboard navigation becomes frustrating or impossible. Good focus management means:
This is the kind of polish that makes your app feel great for everyone.
flushSync
?onbeforeprint
).But be careful! flushSync
is a performance de-optimization. Use it sparingly and only when you really need
synchronous DOM updates. Most of the time, React's normal async updates are
exactly what you want.
Focus management is a subtle but critical part of building accessible,
delightful React apps. flushSync
is a powerful tool for those rare cases when
you need to break out of React's normal update flow and make something happen
right now. Use it wisely, and your users will thank you.
Want to learn more? Check out the official flushSync docs and the Epic React Advanced React APIs workshop.
Delivered straight to your inbox.
Simplify and speed up your app development using React composition
How and why I import react using a namespace
Control Props let you hand over component state to your users—just like a controlled input. Learn to build ultra-flexible UIs at EpicReact.dev.
A basic introduction memoization and how React memoization features work.