Why does my modal focus trap confuse screen readers after an async render?

Hey folks, I’m wiring up a modal in a React-ish app and trying to keep it accessible, but after an async state update the focus trap seems to fight with VoiceOver/NVDA and sometimes announces the page behind the dialog (or focus jumps to the close button twice).

function openModal() {
  setIsOpen(true);
  // content inside modal loads async, so height + focusables change
  requestAnimationFrame(() => {
    const dialog = document.querySelector('[role="dialog"]');
    dialog?.focus();
  });
}

document.addEventListener('focusin', (e) => {
  if (!isOpen) return;
  const dialog = document.querySelector('[role="dialog"]');
  if (dialog && !dialog.contains(e.target)) {
    // bounce focus back in
    dialog.focus();
  }
});

What’s the most reliable pattern to keep focus inside a dynamically-rendered modal without causing screen readers to announce background content or “double focus” jumps?