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
.
Why focus management is tricky in React
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:
const [show, setShow] = useState(false)
<button onClick={() => setShow(true)}>Show</button>
{show ? <input /> : null}
Suppose you want to focus the input as soon as it appears. You might try this:
const inputRef = useRef<HTMLInputElement>(null)
const [show, setShow] = useState(false)
inputRef.current?.focus() // This probably won't work!
{show ? <input ref={inputRef} /> : null}
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.
Why not just use setTimeout or requestAnimationFrame?
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.
inputRef.current?.focus()
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.
Enter 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'
const inputRef = useRef<HTMLInputElement>(null)
const [show, setShow] = useState(false)
inputRef.current?.focus()
{show ? <input ref={inputRef} /> : null}
Now, when you click the button, the input appears and is immediately focused. No
hacks, no guessing, just reliable focus management.
Real-world example: EditableText
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:
- Focus the input and select its text when editing starts
- Return focus to the button when editing ends (by submit, blur, or escape)
Here's how we do it:
import { useRef, useState } from 'react'
import { flushSync } from 'react-dom'
const [edit, setEdit] = useState(false)
const [value, setValue] = useState(initialValue)
const inputRef = useRef<HTMLInputElement>(null)
const buttonRef = useRef<HTMLButtonElement>(null)
setValue(inputRef.current?.value ?? '')
buttonRef.current?.focus()
if (event.key === 'Escape') {
buttonRef.current?.focus()
setValue(event.currentTarget.value)
buttonRef.current?.focus()
inputRef.current?.select()
This approach ensures that focus is always exactly where the user expects it to
be, no matter how quickly they interact with the UI.
Keyboard accessibility: Not just for some users
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:
- When you tab into the editable text, pressing Enter or clicking it should
focus the input and select the text.
- When you finish editing (by submitting, blurring, or pressing Escape), focus
should return to the button so you can keep tabbing through the UI.
This is the kind of polish that makes your app feel great for everyone.
When should you use flushSync
?
- Focus management: When you need to move focus to an element that only
exists after a state update.
- Third-party integrations: When you need to coordinate React updates with
browser APIs or other libraries that expect the DOM to be updated immediately
(e.g.,
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.
Wrap-up
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.