No articles found
Try different keywords or browse our categories
Fix: ReactDOM.render is not a function - React 18 Complete Migration Guide
Learn how to fix the 'ReactDOM.render is not a function' error in React 18. This guide covers the new root API, migration steps, and best practices for React 18 applications.
The ‘ReactDOM.render is not a function’ error is a common issue developers encounter when upgrading to React 18 or when working with modern React applications. This error occurs because React 18 introduced a new React Root API that replaced the legacy ReactDOM.render() method with createRoot().render().
This comprehensive guide explains what changed in React 18, why this error occurs, and provides multiple solutions to fix it in your React applications with clean code examples and directory structure.
What Changed in React 18?
React 18 introduced a new Concurrent Rendering system and a new React Root API. The legacy ReactDOM.render() method was deprecated and replaced with createRoot().render() for better performance, automatic batching, and new features like automatic state updates batching.
Key Changes:
- Legacy API:
ReactDOM.render(element, container) - New API:
root.render(element) - Automatic batching: Better performance for state updates
- Concurrent features: New rendering capabilities
Common Error Messages:
ReactDOM.render is not a functionTypeError: ReactDOM.render is not a functionReact 18: ReactDOM.render is no longer supported
Understanding the Problem
In React 17 and earlier, applications were typically bootstrapped using:
// Legacy React 17 approach
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
However, in React 18, ReactDOM.render was removed from the react-dom package and moved to react-dom/client.
Typical React Project Structure:
my-react-app/
├── package.json
├── src/
│ ├── index.js (or main.jsx for Vite)
│ ├── App.jsx
│ └── components/
│ └── MyComponent.jsx
└── public/
└── index.html
Solution 1: Update to React 18 Root API (Recommended)
Step 1: Update index.js (or main.jsx for Vite)
Before (React 17):
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
After (React 18):
// src/index.js
import React from 'react';
import { createRoot } from 'react-dom/client';
import './index.css';
import App from './App';
const container = document.getElementById('root');
const root = createRoot(container);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
Step 2: Update Package Dependencies
Ensure you have React 18+ installed:
npm install react@latest react-dom@latest
Updated package.json:
{
"name": "my-react-app",
"version": "0.1.0",
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}
Solution 2: Using React DOM Client Import
For Create-React-App Projects:
// src/index.js
import React from 'react';
import { createRoot } from 'react-dom/client';
import './index.css';
import App from './App';
const rootElement = document.getElementById('root');
const root = createRoot(rootElement);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
For TypeScript Projects:
// src/index.tsx
import React from 'react';
import { createRoot } from 'react-dom/client';
import './index.css';
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>
);
Solution 3: Vite Project Configuration
For Vite-based React projects, update the main entry point:
Before (Vite with React 17):
// src/main.jsx
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));
After (Vite with React 18):
// src/main.jsx
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
const container = document.getElementById('root');
const root = createRoot(container);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
Solution 4: Next.js Project Update
For Next.js projects, update your _app.js or _app.jsx:
Before (Next.js with React 17):
// pages/_app.js
import '../styles/globals.css';
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />;
}
export default MyApp;
After (Next.js with React 18):
// pages/_app.js
import '../styles/globals.css';
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />;
}
export default MyApp;
Note: Next.js handles the root API internally, so you typically don’t need to change the client-side rendering in _app.js.
Solution 5: Conditional Rendering for Migration
If you’re migrating gradually, you can create a compatibility function:
// src/compatibility.js
import { createRoot } from 'react-dom/client';
import ReactDOM from 'react-dom';
export function render(element, container) {
if (createRoot) {
// React 18
const root = createRoot(container);
root.render(element);
} else {
// React 17 and below
ReactDOM.render(element, container);
}
}
// Usage
import { render } from './compatibility';
import App from './App';
const container = document.getElementById('root');
render(
<React.StrictMode>
<App />
</React.StrictMode>,
container
);
Solution 6: Testing Environment Updates
Update your testing setup for React 18:
Jest Setup:
// src/setupTests.js
import '@testing-library/jest-dom';
// For React 18, ensure proper cleanup
import { act } from 'react-dom/test-utils';
// Cleanup after each test
afterEach(() => {
act(() => {
// Any cleanup needed
});
});
Testing with React 18:
// src/App.test.js
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});
Solution 7: Server-Side Rendering (SSR) Updates
For SSR applications, update your server-side rendering:
Before (React 17 SSR):
// server.js
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import App from './App';
const html = ReactDOMServer.renderToString(<App />);
After (React 18 SSR):
// server.js
import React from 'react';
import { renderToString } from 'react-dom/server';
import App from './App';
const html = renderToString(<App />);
Note: Server-side rendering APIs remain the same in React 18.
Complete Project Structure After Migration
Standard React App:
my-react-app/
├── package.json
├── package-lock.json
├── node_modules/
├── public/
│ └── index.html
├── src/
│ ├── index.js
│ ├── App.jsx
│ ├── components/
│ │ └── MyComponent.jsx
│ └── utils/
│ └── helpers.js
Vite React App:
my-vite-app/
├── package.json
├── package-lock.json
├── node_modules/
├── public/
├── src/
│ ├── main.jsx
│ ├── App.jsx
│ └── components/
│ └── MyComponent.jsx
└── vite.config.js
Working Code Examples
Complete React 18 Setup:
// src/index.js
import React from 'react';
import { createRoot } from 'react-dom/client';
import './index.css';
import App from './App';
// Get the root container
const container = document.getElementById('root');
// Create root instance
const root = createRoot(container);
// Render the app
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
App Component:
// src/App.jsx
import React, { useState } from 'react';
import './App.css';
function App() {
const [count, setCount] = useState(0);
return (
<div className="App">
<header className="App-header">
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</header>
</div>
);
}
export default App;
Best Practices for React 18 Migration
1. Use Automatic Batching
React 18 automatically batches state updates for better performance:
// React 18 - Automatic batching
function Component() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
// Both updates are batched automatically
setCount(c => c + 1);
setFlag(f => !f);
}
return (
<div>
<p>Count: {count}</p>
<p>Flag: {flag}</p>
<button onClick={handleClick}>Update Both</button>
</div>
);
}
2. Handle Strict Mode Changes
React 18’s Strict Mode double-invokes effects in development:
// Proper cleanup for Strict Mode
function Component() {
useEffect(() => {
const timer = setInterval(() => {
console.log('Timer tick');
}, 1000);
// Cleanup function is essential
return () => {
clearInterval(timer);
};
}, []);
return <div>Component with cleanup</div>;
}
3. Use Suspense for Data Fetching
// With React 18 Suspense
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<DataComponent />
</Suspense>
);
}
Debugging Steps
Step 1: Check React Version
npm list react react-dom
Step 2: Verify Import Statement
// ❌ Wrong
import ReactDOM from 'react-dom';
// ✅ Correct for React 18
import { createRoot } from 'react-dom/client';
Step 3: Clear Cache and Reinstall
rm -rf node_modules package-lock.json
npm install
Step 4: Check for Multiple React Versions
npm ls react --depth=10
Common Migration Issues and Solutions
Issue 1: Third-Party Library Compatibility
Some libraries may still use the old API. Check for updates or use compatibility layers.
Issue 2: Testing Library Updates
Update testing libraries to React 18 compatible versions:
npm install --save-dev @testing-library/react@latest
Issue 3: Custom Render Functions
If you have custom render functions, update them:
// Old custom render
function customRender(ui, options) {
return ReactDOM.render(ui, document.createElement('div'));
}
// New custom render
function customRender(ui, options) {
const container = document.createElement('div');
const root = createRoot(container);
root.render(ui);
return { container, root };
}
Performance Considerations
1. Concurrent Features
React 18’s concurrent rendering can improve performance:
- Automatic batching
- Suspense for data fetching
- Transition updates
2. Memory Management
The new root API provides better memory management:
// Proper cleanup
const root = createRoot(container);
root.render(<App />);
// When unmounting
root.unmount();
Security Considerations
1. Input Validation
Always validate inputs regardless of React version:
function SafeComponent({ userInput }) {
// Validate and sanitize inputs
const sanitizedInput = sanitize(userInput);
return <div>{sanitizedInput}</div>;
}
2. Dependency Updates
Keep React dependencies updated for security patches:
npm audit
npm audit fix
Testing the Migration
1. Development Server
npm start
# Should run without ReactDOM.render errors
2. Production Build
npm run build
# Should complete successfully
3. Unit Tests
npm test
# Should pass with updated React 18 APIs
Alternative Solutions
1. Temporary Compatibility Layer
If immediate migration isn’t possible:
// Temporary compatibility
import { createRoot } from 'react-dom/client';
import ReactDOM from 'react-dom';
const render = createRoot ?
(element, container) => {
const root = createRoot(container);
root.render(element);
} :
ReactDOM.render;
export { render };
2. Downgrade (Not Recommended)
# Only as a temporary measure
npm install react@^17 react-dom@^17
Migration Checklist
- Update React and React DOM to v18+
- Replace
ReactDOM.renderwithcreateRoot().render() - Update import statements to use
react-dom/client - Update testing libraries
- Test all components for compatibility
- Verify production builds work correctly
- Update documentation and team members
Conclusion
The ‘ReactDOM.render is not a function’ error is a natural part of upgrading to React 18, which introduced significant improvements to React’s rendering system. By updating your application to use the new createRoot().render() API, you’ll gain access to React 18’s new features including automatic batching, concurrent rendering, and improved performance.
The migration process is straightforward: update your entry point files to use the new API, ensure you have React 18+ installed, and update any related dependencies. With these changes implemented, your React applications will be fully compatible with React 18 and ready to take advantage of its new capabilities.
Remember to test thoroughly after migration and update your team’s knowledge of the new React 18 patterns and best practices.
Related Articles
Fix: Invalid React hook call. Hooks can only be called inside of the body of a function component
Learn how to fix the 'Invalid hook call' error in React. This guide covers all causes, solutions, and best practices for proper React hook usage with step-by-step examples.
Fix: Module not found: Can't resolve 'react/jsx-runtime' - Complete Solution Guide
Learn how to fix the 'Module not found: Can't resolve react/jsx-runtime' error in React projects. This guide covers causes, solutions, and prevention strategies with step-by-step instructions.
Fix: npm ERR! ERESOLVE unable to resolve dependency tree in React Projects
Learn how to fix the 'npm ERR! ERESOLVE unable to resolve dependency tree' error in React projects. This guide covers causes, solutions, and best practices for dependency management.