본문으로 건너뛰기

useLayoutEffectUntil

Executes a layout effect synchronously until a specified condition is met, then stops permanently. This hook is the synchronous version of useEffectUntil, using useLayoutEffect to run before the browser paints. It's ideal for DOM measurements and manipulations that must complete before the user sees the screen update.

When to Use Over useEffectUntil

  • DOM Measurements: When you need accurate layout measurements before paint
  • Style Calculations: Computing styles based on element dimensions
  • Scroll Position Management: Restoring scroll positions without visual jumps
  • Animation Setup: Initializing animation states to prevent flicker
  • Focus Management: Setting focus without visual delays

Behavior

  • Runs synchronously after DOM mutations but before browser paint
  • Blocks browser painting until completion (use sparingly)
  • Once the effect returns true, it permanently stops executing
  • The completion state persists across re-renders

Performance Considerations

Since this runs synchronously and blocks painting, it can impact performance. Only use when the synchronous behavior is necessary to prevent visual issues.

Signature

const useLayoutEffectUntil: <Dependencies extends DependencyList>(effect: () => boolean, dependencies?: Dependencies) => void

Parameters

NameTypeDescription
effect-A function that performs synchronous side effects and returns true when done
dependencies-Optional dependency array that triggers re-execution when changed

Examples

Example 1

// Measure and adjust layout until it fits
useLayoutEffectUntil(() => {
const element = ref.current;
if (!element) return false;

const { width } = element.getBoundingClientRect();
if (width > maxWidth) {
element.style.fontSize = `${currentSize - 1}px`;
setCurrentSize(prev => prev - 1);
return false; // Keep adjusting
}
return true; // Fits perfectly
}, [currentSize, maxWidth]);

// Restore scroll position without flicker
useLayoutEffectUntil(() => {
const savedPosition = sessionStorage.getItem('scrollPos');
if (!savedPosition) return true;

window.scrollTo(0, parseInt(savedPosition));
sessionStorage.removeItem('scrollPos');
return true; // Done restoring
}, []);

// Initialize animation state before first paint
useLayoutEffectUntil(() => {
const elements = document.querySelectorAll('.animate');
if (elements.length === 0) return false;

elements.forEach(el => {
el.style.opacity = '0';
el.style.transform = 'translateY(20px)';
});
setAnimationReady(true);
return true; // Initialization complete
}, []);

Playground

// Measure and adjust layout until it fits
useLayoutEffectUntil(() => {
const element = ref.current;
if (!element) return false;

const { width } = element.getBoundingClientRect();
if (width > maxWidth) {
  element.style.fontSize = `${currentSize - 1}px`;
  setCurrentSize(prev => prev - 1);
  return false; // Keep adjusting
}
return true; // Fits perfectly
}, [currentSize, maxWidth]);

// Restore scroll position without flicker
useLayoutEffectUntil(() => {
const savedPosition = sessionStorage.getItem('scrollPos');
if (!savedPosition) return true;

window.scrollTo(0, parseInt(savedPosition));
sessionStorage.removeItem('scrollPos');
return true; // Done restoring
}, []);

// Initialize animation state before first paint
useLayoutEffectUntil(() => {
const elements = document.querySelectorAll('.animate');
if (elements.length === 0) return false;

elements.forEach(el => {
  el.style.opacity = '0';
  el.style.transform = 'translateY(20px)';
});
setAnimationReady(true);
return true; // Initialization complete
}, []);