flushSync
Forces React to flush all pending updates synchronously, ensuring the DOM is immediately updated.
import { flushSync } from 'react-dom';
flushSync(() => {
setState(newValue);
});
// DOM is now updated
flushSync can significantly hurt performance. Use sparingly and only when necessary.
Reference
flushSync(callback?)
Call flushSync to force React to flush any pending work and update the DOM synchronously.
/src/shared/ReactDOMFlushSync.js:17-50
flushSync(() => {
setCount(count + 1);
});
Parameters
Optional callback function containing state updates. React will immediately flush these updates and apply them to the DOM before flushSync returns.flushSync(() => {
setState(newValue);
});
// State is now committed to DOM
// Or call without callback to flush pending updates
flushSync();
Returns
Returns nothing. All updates are flushed synchronously before the function returns.
When to Use flushSync
1. Third-Party Integrations
When integrating with third-party libraries that need immediate DOM access:
import { flushSync } from 'react-dom';
import { useState, useEffect } from 'react';
import thirdPartyLibrary from 'some-library';
function Chart({ data }) {
const [container, setContainer] = useState(null);
useEffect(() => {
if (!container) return;
// Ensure React has updated the DOM before library accesses it
flushSync(() => {
setContainer(container);
});
const chart = thirdPartyLibrary.init(container);
chart.render(data);
return () => chart.destroy();
}, [container, data]);
return <div ref={setContainer} />;
}
2. Print Functionality
Ensure all updates are applied before printing:
import { flushSync } from 'react-dom';
import { useState } from 'react';
function Report() {
const [showDetails, setShowDetails] = useState(false);
const handlePrint = () => {
// Expand all sections before printing
flushSync(() => {
setShowDetails(true);
});
// DOM is now updated, safe to print
window.print();
};
return (
<div>
<button onClick={handlePrint}>Print Report</button>
{showDetails && <DetailedReport />}
</div>
);
}
3. Focus Management
When you need to focus an element immediately after rendering:
import { flushSync } from 'react-dom';
import { useState, useRef } from 'react';
function Dialog({ onClose }) {
const [isOpen, setIsOpen] = useState(false);
const inputRef = useRef(null);
const openDialog = () => {
flushSync(() => {
setIsOpen(true);
});
// Input is now in DOM, safe to focus
inputRef.current?.focus();
};
if (!isOpen) {
return <button onClick={openDialog}>Open Dialog</button>;
}
return (
<div>
<input ref={inputRef} />
<button onClick={onClose}>Close</button>
</div>
);
}
4. Measuring Layout
When you need to measure an element immediately after an update:
import { flushSync } from 'react-dom';
import { useState, useRef } from 'react';
function ExpandableSection({ children }) {
const [isExpanded, setIsExpanded] = useState(false);
const contentRef = useRef(null);
const toggle = () => {
flushSync(() => {
setIsExpanded(!isExpanded);
});
// Measure the new height immediately
if (contentRef.current) {
const height = contentRef.current.offsetHeight;
console.log('New height:', height);
}
};
return (
<div>
<button onClick={toggle}>Toggle</button>
{isExpanded && <div ref={contentRef}>{children}</div>}
</div>
);
}
5. Drag and Drop
Ensure immediate updates during drag operations:
import { flushSync } from 'react-dom';
import { useState } from 'react';
function DraggableList({ items }) {
const [list, setList] = useState(items);
const [draggedIndex, setDraggedIndex] = useState(null);
const handleDrop = (targetIndex) => {
if (draggedIndex === null) return;
const newList = [...list];
const [removed] = newList.splice(draggedIndex, 1);
newList.splice(targetIndex, 0, removed);
// Update list immediately for smooth drag feedback
flushSync(() => {
setList(newList);
setDraggedIndex(null);
});
};
return (
<div>
{list.map((item, index) => (
<div
key={item.id}
draggable
onDragStart={() => setDraggedIndex(index)}
onDrop={() => handleDrop(index)}
onDragOver={(e) => e.preventDefault()}
>
{item.name}
</div>
))}
</div>
);
}
How React Normally Works
By default, React batches updates for performance:
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
// These are batched together
setCount(count + 1);
setCount(count + 2);
setCount(count + 3);
// React renders once with final value
};
return <button onClick={handleClick}>{count}</button>;
}
With flushSync (Slower)
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
flushSync(() => {
setCount(count + 1);
});
// React renders here (render #1)
flushSync(() => {
setCount(count + 2);
});
// React renders here (render #2)
flushSync(() => {
setCount(count + 3);
});
// React renders here (render #3)
// Three renders instead of one!
};
return <button onClick={handleClick}>{count}</button>;
}
import { flushSync } from 'react-dom';
import { useState } from 'react';
function PerformanceTest() {
const [count, setCount] = useState(0);
const normalUpdate = () => {
const start = performance.now();
// Batched - fast
for (let i = 0; i < 1000; i++) {
setCount(i);
}
const duration = performance.now() - start;
console.log('Normal:', duration, 'ms'); // ~1ms
};
const syncUpdate = () => {
const start = performance.now();
// Synchronous - slow
for (let i = 0; i < 1000; i++) {
flushSync(() => {
setCount(i);
});
}
const duration = performance.now() - start;
console.log('Sync:', duration, 'ms'); // ~500ms!
};
return (
<div>
<button onClick={normalUpdate}>Normal Update</button>
<button onClick={syncUpdate}>Sync Update</button>
<div>Count: {count}</div>
</div>
);
}
Common Pitfalls
1. Using in Event Handlers (Usually Unnecessary)
// Bad: flushSync not needed here
function handleClick() {
flushSync(() => {
setCount(count + 1);
});
}
// Good: React batches automatically
function handleClick() {
setCount(count + 1);
}
2. Using in Effects
// Bad: Can cause issues
useEffect(() => {
flushSync(() => {
setState(value);
});
}, [value]);
// Good: Let React batch naturally
useEffect(() => {
setState(value);
}, [value]);
3. Nesting flushSync Calls
// Bad: Very slow!
flushSync(() => {
flushSync(() => {
setState(value);
});
});
// Good: Single flushSync
flushSync(() => {
setState(value);
});
4. Using for Animation
// Bad: Causes jank
for (let i = 0; i < 100; i++) {
flushSync(() => {
setPosition(i);
});
}
// Good: Use CSS animations or requestAnimationFrame
function animate() {
let i = 0;
function step() {
setPosition(i++);
if (i < 100) {
requestAnimationFrame(step);
}
}
requestAnimationFrame(step);
}
Alternatives to flushSync
1. Use Effects Instead
// Instead of this:
flushSync(() => {
setState(value);
});
measureElement();
// Consider this:
useEffect(() => {
measureElement();
}, [value]);
2. Use Layout Effects
import { useLayoutEffect } from 'react';
// Runs synchronously after DOM updates
useLayoutEffect(() => {
const width = divRef.current.offsetWidth;
setWidth(width);
}, [dependency]);
import { useRef } from 'react';
function Component() {
const countRef = useRef(0);
const handleClick = () => {
// Immediate update without re-render
countRef.current += 1;
console.log(countRef.current); // Updated immediately
};
}
4. Use Callback Refs
function Component() {
const [height, setHeight] = useState(0);
const measureRef = useCallback((node) => {
if (node !== null) {
setHeight(node.offsetHeight);
}
}, []);
return <div ref={measureRef}>Content</div>;
}
TypeScript
import { flushSync } from 'react-dom';
import { useState } from 'react';
function Component() {
const [count, setCount] = useState<number>(0);
const handleUpdate = (): void => {
// Type-safe usage
flushSync(() => {
setCount(count + 1);
});
// Or without callback
flushSync();
};
return <button onClick={handleUpdate}>{count}</button>;
}
Testing
import { render, screen, waitFor } from '@testing-library/react';
import { flushSync } from 'react-dom';
import userEvent from '@testing-library/user-event';
test('updates synchronously', () => {
let count = 0;
function Counter() {
const [value, setValue] = useState(0);
return (
<button
onClick={() => {
flushSync(() => {
setValue(value + 1);
});
count = value + 1;
}}
>
{value}
</button>
);
}
render(<Counter />);
const button = screen.getByRole('button');
userEvent.click(button);
// With flushSync, count is updated synchronously
expect(count).toBe(1);
expect(button).toHaveTextContent('1');
});
Debugging
Enable logging to see when flushSync is called:
import { flushSync } from 'react-dom';
function logFlushSync(callback) {
console.log('[flushSync] Start');
const start = performance.now();
flushSync(callback);
const duration = performance.now() - start;
console.log(`[flushSync] End (${duration.toFixed(2)}ms)`);
}
// Usage
logFlushSync(() => {
setState(newValue);
});
Strict Mode Compatibility
flushSync works in Strict Mode but may cause warnings if used in render:
function Component() {
// Warning: flushSync called during render!
flushSync(() => {
setState(value);
});
return <div>Content</div>;
}
// Move to event handlers or effects instead
function Component() {
const handleClick = () => {
flushSync(() => {
setState(value);
});
};
return <button onClick={handleClick}>Click</button>;
}
Best Practices
- Use sparingly - Most apps don’t need flushSync
- Measure performance - Profile before and after
- Consider alternatives - Effects, layout effects, refs
- Document usage - Explain why it’s necessary
- Avoid in loops - Extremely slow
- Test without it first - May not be needed
Browser Compatibility
Works in all browsers that support React:
- Chrome 90+
- Firefox 88+
- Safari 14.1+
- Edge 90+