Fix Error: Target container is not a DOM element. in React
This error fires when ReactDOM.createPortal or ReactDOM.render targets a DOM element that does not exist yet. The element may be missing from the HTML, the script may run before the DOM is ready, or the portal target ID is misspelled. Fix it by ensuring the target element exists in the HTML before the portal renders, or by deferring portal rendering to useEffect.
Reading the Stack Trace
Here's what each line means:
- at createPortal (node_modules/react-dom/cjs/react-dom.development.js:254:11): ReactDOM.createPortal throws because the target container passed to it is null, not a valid DOM element.
- at Modal (webpack-internal:///./src/components/Modal.tsx:10:10): Modal.tsx line 10 calls createPortal with document.getElementById('modal-root'), which returns null because the element does not exist.
- at mountIndeterminateComponent (node_modules/react-dom/cjs/react-dom.development.js:20069:13): The component is mounting for the first time and immediately crashes because the portal target is missing.
Common Causes
1. Missing DOM element in HTML
The index.html file does not contain the target element that the portal tries to render into.
// index.html only has <div id="root"></div>
// Missing <div id="modal-root"></div>
export default function Modal({ children }) {
return createPortal(
<div className="modal">{children}</div>,
document.getElementById('modal-root')!
);
}
2. Typo in element ID
The ID string passed to getElementById does not match the ID in the HTML due to a typo.
// HTML has: <div id="modal-root"></div>
// Component uses wrong ID:
const target = document.getElementById('modalRoot');
return createPortal(<div>{children}</div>, target!);
3. SSR environment without DOM
During server-side rendering, document is not available, so getElementById returns null and createPortal crashes.
export default function Modal({ children }) {
const target = document.getElementById('modal-root');
return createPortal(<div>{children}</div>, target!);
}
The Fix
Defer the portal target lookup to useEffect, which only runs on the client after the DOM is ready. If the target element does not exist, create it dynamically. Render null until the container is available, preventing the crash entirely.
export default function Modal({ children }) {
return createPortal(
<div className="modal">{children}</div>,
document.getElementById('modal-root')!
);
}
import { createPortal } from 'react-dom';
import { useState, useEffect } from 'react';
export default function Modal({ children }: { children: React.ReactNode }) {
const [container, setContainer] = useState<HTMLElement | null>(null);
useEffect(() => {
let el = document.getElementById('modal-root');
if (!el) {
el = document.createElement('div');
el.id = 'modal-root';
document.body.appendChild(el);
}
setContainer(el);
}, []);
if (!container) return null;
return createPortal(
<div className="modal">{children}</div>,
container
);
}
Testing the Fix
import { render, screen } from '@testing-library/react';
import Modal from './Modal';
describe('Modal', () => {
afterEach(() => {
const el = document.getElementById('modal-root');
if (el) el.remove();
});
it('creates modal-root if missing', () => {
render(<Modal>Hello</Modal>);
expect(document.getElementById('modal-root')).toBeInTheDocument();
});
it('renders children into the portal', () => {
render(<Modal><p>Modal content</p></Modal>);
expect(screen.getByText('Modal content')).toBeInTheDocument();
});
it('does not crash when rendered on the server', () => {
expect(() => render(<Modal>Test</Modal>)).not.toThrow();
});
});
Run your tests:
npm test
Pushing Through CI/CD
git checkout -b fix/react-portal-target-error,git add src/components/Modal.tsx src/components/__tests__/Modal.test.tsx,git commit -m "fix: defer portal target lookup to useEffect and create if missing",git push origin fix/react-portal-target-error
Your CI config should look something like this:
name: CI
on:
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm test -- --coverage
- run: npm run build
The Full Manual Process: 18 Steps
Here's every step you just went through to fix this one bug:
- Notice the error alert or see it in your monitoring tool
- Open the error dashboard and read the stack trace
- Identify the file and line number from the stack trace
- Open your IDE and navigate to the file
- Read the surrounding code to understand context
- Reproduce the error locally
- Identify the root cause
- Write the fix
- Run the test suite locally
- Fix any failing tests
- Write new tests covering the edge case
- Run the full test suite again
- Create a new git branch
- Commit and push your changes
- Open a pull request
- Wait for code review
- Merge and deploy to production
- Monitor production to confirm the error is resolved
Total time: 30-60 minutes. For one bug.
Or Let bugstack Fix It in Under 2 minutes
Every step above? bugstack does it automatically.
Step 1: Install the SDK
npm install bugstack-sdk
Step 2: Initialize
import { initBugStack } from 'bugstack-sdk'
initBugStack({ apiKey: process.env.BUGSTACK_API_KEY })
Step 3: There is no step 3.
bugstack handles everything from here:
- Captures the stack trace and request context
- Pulls the relevant source files from your GitHub repo
- Analyzes the error and understands the code context
- Generates a minimal, verified fix
- Runs your existing test suite
- Pushes through your CI/CD pipeline
- Deploys to production (or opens a PR for review)
Time from error to fix deployed: Under 2 minutes.
Human involvement: zero.
Try bugstack Free →No credit card. 5-minute setup. Cancel anytime.
Deploying the Fix (Manual Path)
- Run the test suite locally to confirm the portal renders correctly.
- Open a pull request with the deferred portal target lookup.
- Wait for CI checks to pass on the PR.
- Have a teammate review and approve the PR.
- Merge to main and verify modals work in staging.
Frequently Asked Questions
BugStack validates that the portal target exists or is created dynamically, runs your test suite including SSR scenarios, and confirms no layout regressions before marking it safe.
BugStack never pushes directly to production. Every fix goes through a pull request with full CI checks, so your team can review it before merging.
Adding a static element to index.html is simpler and slightly faster. Dynamic creation is better when you cannot modify the HTML template, such as in micro-frontend or library scenarios.
Yes. Even though the portal renders outside the parent DOM hierarchy, React events still bubble through the React component tree as if the portal were inside its parent.