Articles

Mastering Focus Management in React with `flushSync`

Kent C. Dodds
Kent C. Dodds

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:


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.

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.


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.

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'
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.

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'
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.

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.

Get my free 7-part email course on React!

Delivered straight to your inbox.