Understanding the ReactDOM Package
The react-dom package provides DOM-specific methods that serve as the bridge between React's component-based architecture and the browser's Document Object Model. These methods handle the critical task of rendering React elements to actual DOM nodes and managing their lifecycle.
ReactDOM handles the initialization of React applications in the browser, managing updates to existing DOM structures, and providing escape hatches when direct DOM access becomes necessary. The package separates DOM-specific functionality from React's core component logic, allowing React to remain agnostic about its rendering target--whether that's a browser DOM, native mobile views, or other environments.
This separation of concerns matters significantly for application architecture. By keeping DOM operations isolated in a dedicated package, React's core can focus on state management and component lifecycle while ReactDOM handles the platform-specific rendering details. This approach enables React to power not just web applications but also React Native for mobile and potentially other rendering targets in the future.
For teams building modern web applications, understanding ReactDOM is essential for creating performant, maintainable interfaces that leverage the full power of React's rendering capabilities.
Core Legacy Methods
These methods represent the foundational ReactDOM API that powered React applications for years. While still available for backward compatibility, React 18 introduced modern alternatives that represent the recommended approach for new projects.
ReactDOM.render()
ReactDOM.render() takes a React element and renders it into the supplied container, returning a reference to the component or null for stateless components. This method was the primary entry point for React applications before React 18.
ReactDOM.render(element, container[, callback]);
The method creates a direct connection between React's virtual DOM representation and an existing DOM node. When called for the first time, it mounts the component tree into the container. Subsequent calls perform updates using React's diffing algorithm to minimize actual DOM manipulations. The optional callback executes after render or update completes.
Understanding render behavior is essential: initial render creates the component tree in the container, later calls perform incremental updates based on state changes, and the method does not modify the container node itself--only its children. This distinction matters when integrating React into existing applications.
ReactDOM.hydrate()
ReactDOM.hydrate() serves the same fundamental purpose as render() but is specifically designed for server-side rendered content. It attaches event listeners to existing markup rather than creating new DOM nodes.
ReactDOM.hydrate(element, container[, callback]);
The hydration process is critical for server-side rendering workflows where the initial HTML is generated on the server and then "hydrated" into a fully interactive React application on the client. This method expects pre-rendered markup from ReactDOMServer and warns about content mismatches between server and client renders. Unlike render, hydrate cannot automatically patch mismatches--you must resolve them in your component code or use the suppressHydrationWarning prop for specific elements.
ReactDOM.findDOMNode()
ReactDOM.findDOMNode() provides direct access to the underlying DOM node for a mounted React component. This method served as an escape hatch when direct DOM manipulation was necessary.
ReactDOM.findDOMNode(component);
The method returns the DOM node corresponding to the mounted component. For components rendering null or false, it returns null. For string renders, it returns a text node. For fragments, it returns the first non-empty child. This method was necessary in scenarios requiring direct DOM access before refs were widely adopted--such as measuring element dimensions, managing focus, or integrating with third-party libraries that expected raw DOM nodes.
However, findDOMNode has significant limitations: it only works on mounted components, cannot be used on function components, was deprecated in React 17 StrictMode, and was completely removed in React 19. If you're maintaining legacy codebases, you'll need to migrate to refs.
ReactDOM.unmountComponentAtNode()
This method removes a mounted React component from the DOM and performs cleanup of event handlers and component state.
ReactDOM.unmountComponentAtNode(container);
The cleanup process ensures that all resources associated with the mounted component are properly released, preventing memory leaks in dynamically mounted scenarios. The method returns true if a component was unmounted and false if no component was mounted. Use this method when removing dynamically mounted components such as modals, overlays, or conditionally rendered heavy components that you want to completely remove from the DOM.
Modern ReactDOM Methods
React 18 introduced new approaches that replace the legacy render and hydrate methods, providing better control over the rendering process and improved concurrent rendering support.
createRoot and hydrateRoot
The new client API introduces createRoot() and hydrateRoot() as replacements for the legacy render() and hydrate() methods. These functions return a root object with an unmount() method for cleanup.
import { createRoot } from 'react-dom/client';
const root = createRoot(container);
root.render(element);
// Cleanup when done
root.unmount();
The new root API offers several benefits over legacy methods. It returns a root object with explicit cleanup methods, provides better integration with concurrent features, enables improved error boundaries, and implements automatic batching of updates. For server-rendered content, use hydrateRoot instead:
import { hydrateRoot } from 'react-dom/client';
const root = hydrateRoot(container, element);
root.unmount();
ReactDOM.createPortal()
Portals provide a mechanism to render children into a DOM node that exists outside the normal DOM hierarchy of the parent component. This is essential for modals, tooltips, and other overlay components.
ReactDOM.createPortal(child, container);
The portal creates a visual and event bubbling separation between the React component tree and the actual DOM placement. Despite rendering into a separate DOM node, event bubbling still works through the React tree, so clicks inside a portal will still bubble up through the React component hierarchy. The portal container should be placed directly under the body element in your HTML, outside the main application root.
Common portal patterns include modals that need to escape parent overflow constraints, tooltips that should appear above all other content, dropdown menus that need to avoid z-index stacking context issues, and floating action buttons that should overlay page content. For example, a modal portal renders into a dedicated modal-root div, allowing CSS styles to control modal positioning independently of the parent component structure.
When building complex user interfaces, understanding portal patterns becomes crucial for maintaining clean DOM structures while achieving sophisticated visual effects.
ReactDOM.flushSync()
flushSync forces React to synchronously flush any updates inside the provided callback, ensuring immediate DOM updates before continuing execution.
import { flushSync } from 'react-dom';
flushSync(() => {
setCount(count + 1);
});
// DOM is updated immediately after this point
This method is useful when you need to coordinate React updates with third-party code that expects synchronous DOM changes--such as legacy DOM measurement libraries or browser APIs that read layout state immediately after mutations. However, flushSync can significantly impact performance by forcing React to update synchronously rather than batching updates optimally. It may also force Suspense boundaries to show fallbacks earlier than intended. Use sparingly and only when synchronous updates are genuinely required for correctness, not convenience.
Performance optimization is a key consideration when building high-performing web applications. Understanding when to use flushSync versus allowing React's automatic batching helps create responsive user interfaces that maintain smooth interactions.
Working with DOM References
Modern React emphasizes refs over findDOMNode for direct DOM access, providing a more explicit and reliable pattern that works across all component types.
Using Refs for DOM Access
Refs provide a direct reference to DOM nodes without the pitfalls of the deprecated findDOMNode method. For function components, use useRef to create a ref object that persists across renders:
import { useRef, useEffect } from 'react';
function InputComponent() {
const inputRef = useRef(null);
useEffect(() => {
// Direct DOM access through ref
inputRef.current.focus();
}, []);
return <input ref={inputRef} type="text" />;
}
For class components, createRef returns a ref object that must be attached in the render method:
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
componentDidMount() {
this.myRef.current.focus();
}
render() {
return <div ref={this.myRef}>My Component</div>;
}
}
Refs offer several advantages over findDOMNode: they are more explicit and predictable, work with both function and class components, produce no deprecation warnings, and have better performance characteristics. The ref API also integrates better with React's lifecycle and concurrent rendering features.
Callback Refs
For more complex scenarios, callback refs provide additional flexibility by allowing inline functions that receive the DOM node directly:
<input
ref={(node) => {
if (node) {
node.focus();
node.addEventListener('change', handleChange);
} else {
// Cleanup when ref is null
}
}}
/>
Callback refs are useful when you need immediate access to the DOM node during assignment, need to attach multiple refs to different elements, or want to integrate with APIs that expect function callbacks. However, for most cases, object refs created with useRef or createRef are simpler and more maintainable.
Common Patterns and Use Cases
Dynamic Mounting
Mount components dynamically for modals, lazy-loaded content, or runtime-created interfaces. This pattern is essential when you need to render components that weren't part of the initial application structure:
import { createRoot } from 'react-dom/client';
function openModal(content) {
// Create container dynamically
const container = document.createElement('div');
document.body.appendChild(container);
// Create root and render
const root = createRoot(container);
root.render(content);
// Return cleanup function
return () => {
root.unmount();
document.body.removeChild(container);
};
}
// Usage
const close = openModal(<MyModal onClose={() => close()} />);
This pattern creates a container element, renders the component into it, and returns a cleanup function that unmounts the component and removes the container from the DOM. Always ensure cleanup is called to prevent memory leaks.
Portals for Overlays
Use portals for modals, tooltips, and other overlays that need to visually break out of their parent container's constraints:
import { createPortal } from 'react-dom';
function Modal({ isOpen, children, onClose }) {
if (!isOpen) return null;
return createPortal(
<div className="modal-backdrop" onClick={onClose}>
<div className="modal-content" onClick={e => e.stopPropagation()}>
{children}
</div>
</div>,
document.body
);
}
Portal patterns help avoid z-index issues by rendering outside the normal stacking context. Always include proper focus management and keyboard navigation for accessible modals.
SSR with Hydration
For server-rendered applications, proper hydration is essential for client interactivity:
import { hydrateRoot } from 'react-dom/client';
import { startTransition } from 'react';
async function hydrate() {
const response = await fetch('/api/initial-data');
const initialData = await response.json();
const root = hydrateRoot(
document.getElementById('root'),
<App initialData={initialData} />
);
// Mark hydration as complete
startTransition(() => {
root.render(<App initialData={initialData} />);
});
}
Proper SSR hydration handles the transition from server-rendered HTML to interactive client components, ensuring a fast initial paint while maintaining full interactivity once hydration completes.
These patterns form the foundation of robust React application development, enabling developers to build complex, interactive interfaces that perform well and maintain clean code architecture.
Best Practices
Do: Use Modern APIs
Prefer createRoot/hydrateRoot over render/hydrate, and refs over findDOMNode. New projects should always use the React 18+ APIs for better concurrent rendering support and future compatibility:
// Modern approach (React 18+)
import { createRoot } from 'react-dom/client';
const root = createRoot(container);
root.render(element);
// Legacy approach (avoid)
import { render } from 'react-dom';
render(element, container);
Don't: Mix Legacy and Modern APIs
Stick to one approach throughout your application. Mixing legacy and modern APIs on the same root can cause undefined behavior and hydration issues. If you're migrating an existing application, update all mount points consistently rather than using both patterns side by side.
Do: Clean Up Properly
Always unmount components when removing them from the DOM. With the modern API, use the root.unmount() method:
function DynamicComponent() {
const handleUnmount = () => {
root.unmount();
// Container element remains but is now empty
};
return <button onClick={handleUnmount}>Unmount</button>;
}
Don't: Rely on findDOMNode
The deprecated findDOMNode was removed entirely in React 19. Migrate existing code to use refs:
// Instead of this (deprecated, removed in React 19)
const node = ReactDOM.findDOMNode(this.refs.myComponent);
// Use this (modern approach)
const node = this.myRef.current;
For class components, use createRef() in the constructor. For function components, use useRef() at the component scope. Both patterns provide more explicit and maintainable DOM access.
Following these best practices ensures your React applications remain maintainable, performant, and aligned with the broader React ecosystem. Teams investing in modern web development practices benefit from cleaner codebases and easier future upgrades.
Frequently Asked Questions
What replaced ReactDOM.render in React 18?
React 18 introduced createRoot() and hydrateRoot() from 'react-dom/client'. These methods return a root object with render() and unmount() methods, providing better integration with concurrent features.
How do I migrate from findDOMNode to refs?
Replace ReactDOM.findDOMNode(this) with this.myRef.current where myRef is created using useRef (function components) or createRef (class components). The ref provides more explicit DOM access.
When should I use createPortal?
Use portals for modals, tooltips, dropdown menus, and overlay components that need to visually break out of their parent container's overflow, z-index, or positioning context.
What causes hydration mismatches?
Hydration mismatches occur when server-rendered HTML differs from what React would render on the client, such as with timestamps, browser-specific content, or random values generated during render.
Is findDOMNode completely removed?
findDOMNode was deprecated in React 17 StrictMode and completely removed in React 19. Use refs instead for direct DOM access. This change enforces more explicit and predictable component patterns.
Sources
- LogRocket Blog - Managing DOM components with ReactDOM - Comprehensive tutorial covering ReactDOM methods with practical examples
- React GitHub Issue #28926 - React 19 findDOMNode removal documentation and migration strategies
- React Legacy Documentation - ReactDOM - Official API reference and deprecation notices