Today we are looking at how to interact directly with the browser DOM using modern React 19 DOM APIs while avoiding deprecated patterns.
What you'll learn
- How to render modals and tooltips outside the root tree using
createPortal. - When and why to force synchronous DOM mutations with
flushSync. - Navigating the entry points between client rendering (
react-dom/client) and server rendering (react-dom/server). - Safely upgrading away from removed React 19 APIs like
renderandhydrate.
Rendering Outside the Root with createPortal
Sometimes your component hierarchy forces a DOM node to be clipped by overflow settings or positioned weirdly. createPortal lets you punch a hole through your tree and render children anywhere in the DOM.
import { useState } from 'react';
import { createPortal } from 'react-dom';
export function ModalBadge() {
const [isOpen, setIsOpen] = useState(false);
return (
<div style={{ position: 'relative' }}>
<button onClick={() => setIsOpen(true)}>Open Modal</button>
{isOpen &&
createPortal(
<div className="modal-backdrop" onClick={() => setIsOpen(false)}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<p>Portal content rendered directly in document.body</p>
<button onClick={() => setIsOpen(false)}>Close</button>
</div>
</div>,
document.body
)}
</div>
);
}
Speed Note: Portals only change the DOM node location, not the React context tree. Events still bubble up through your React component tree, even if the DOM nodes are miles apart.
Forcing Synchronous Layout with flushSync
React usually batches state updates for performance. Rarely, you might need to force the browser to recalculate layout immediately after a state change before the next line of code runs. flushSync handles this edge case.
import { useState, useRef } from 'react';
import { flushSync } from 'react-dom';
export function SyncTodoList() {
const [todos, setTodos] = useState(['Learn React', 'Build an app']);
const [text, setText] = useState('');
const listRef = useRef<HTMLUListElement>(null);
function handleAdd() {
flushSync(() => {
setTodos((prev) => [...prev, text]);
setText('');
});
// DOM is guaranteed to be updated right here
if (listRef.current) {
listRef.current.scrollTop = listRef.current.scrollHeight;
}
}
return (
<div>
<ul ref={listRef} style={{ maxHeight: '100px', overflowY: 'auto' }}>
{todos.map((t, i) => (
<li key={i}>{t}</li>
))}
</ul>
<input value={text} onChange={(e) => setText(e.target.value)} />
<button onClick={handleAdd}>Add Todo</button>
</div>
);
}
Gotcha:
flushSynchurts performance because it forces React to flush updates early and interrupt batching. Use it only when integrating with third-party libraries that rely on immediate DOM measurements.
Clean Client Entry Points with react-dom/client
In React 19, legacy top-level rendering methods are gone. Your entry point should always use createRoot for client-side applications.
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
const container = document.getElementById('root');
if (!container) {
throw new Error('Failed to find the root element');
}
const root = createRoot(container);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
Migration Tip: If you see errors about
ReactDOM.renderorReactDOM.hydrate, you are likely upgrading an older codebase. ReplaceReactDOM.render(with, container) createRoot(container).render(.)
Removing Legacy APIs in React 19
React 19 cleaned up legacy APIs that have been deprecated for years. If your codebase relies on findDOMNode or hydrate, your build will break until you refactor.
findDOMNodeis removed. Attach auseRefdirectly to your DOM element instead.hydrateis replaced entirely byhydrateRootfromreact-dom/client.unmountComponentAtNodeis replaced by callingroot.unmount().
// OLD (Removed in React 19)
// ReactDOM.hydrate(<App />, container);
// NEW (React 19 standard)
import { hydrateRoot } from 'react-dom/client';
hydrateRoot(container, <App />);
Summary & Next Steps
React DOM APIs give you fine-grained control over browser interaction when standard component rendering isn't quite enough. Keep your bundle fast, avoid synchronous work unless necessary, and keep your client entry points updated to React 19 standards.
- Audit your codebase for deprecated APIs like
findDOMNodeor oldrendercalls. - Experiment with
createPortalfor accessible overlays and dropdown menus.