Preact Compatibility with React
How to use Preact as a drop-in React replacement and which React libraries work with preact/compat.
Preact is a 4KB alternative to React with the same core API. The preact/compat module provides a compatibility layer that lets you use most React libraries unchanged.
Setting Up preact/compat
With Vite
// vite.config.ts
import { defineConfig } from "vite";
import preact from "@preact/preset-vite";
export default defineConfig({
plugins: [preact()],
resolve: {
alias: {
react: "preact/compat",
"react-dom": "preact/compat",
"react-dom/client": "preact/compat",
},
},
});
With Next.js (experimental)
npm install next preact preact-render-to-string
// next.config.js
module.exports = {
reactProductionProfiling: false,
webpack: (config) => {
config.resolve.alias = {
...config.resolve.alias,
react: "preact/compat",
"react-dom": "preact/compat",
};
return config;
},
};
What Works
| Library |
Works? |
Notes |
| React Router |
✅ |
Full support via preact/compat |
| Redux |
✅ |
Works unchanged |
| Zustand |
✅ |
Works unchanged |
| React Query / TanStack Query |
✅ |
Works unchanged |
| Framer Motion |
⚠️ |
Partial — some animation features may differ |
| React Testing Library |
✅ |
Use @testing-library/preact instead |
| styled-components |
⚠️ |
Works but increases bundle size significantly |
| MUI (Material UI) |
⚠️ |
Partial — relies on some React internals |
| Radix UI |
✅ |
Works via preact/compat |
| Headless UI |
✅ |
Works via preact/compat |
Key API Differences
| Feature |
React |
Preact |
Description |
| Event system |
SyntheticEvent |
Native DOM events |
Preact passes real browser events directly |
| Event pooling |
Yes (deprecated) |
No |
No e.persist() needed in Preact |
| onChange |
Fires on input change |
Fires on input change |
Preact uses native input event for consistency |
| SVG support |
Full |
Full |
Preact handles SVG attributes correctly |
| String refs |
Supported (legacy) |
Not supported |
Use callback refs or useRef instead |
| PropTypes |
Built-in (deprecated) |
Not included |
Use TypeScript for type safety |
Lifecycle Mapping
| React Class Lifecycle |
Preact / preact/compat |
componentDidMount |
useEffect(() => {}, []) or onMount in class |
componentDidUpdate |
useEffect(() => {}, [deps]) |
componentWillUnmount |
useEffect cleanup or onUnmount in class |
shouldComponentUpdate |
React.memo or memo from preact/compat |
getDerivedStateFromProps |
useState with initializer or getDerivedStateFromProps in class |
Preact-Only Features
// Hook into VDOM diffing with hooks
import { options } from "preact";
// Log every render
options.render = (vdom) => {
console.log("rendering:", vdom.type);
};
// Preact Devtools
import "preact/debug"; // Adds helpful development warnings
Testing with Preact
// Use @testing-library/preact instead of @testing-library/react
import { render, screen, fireEvent } from "@testing-library/preact";
import userEvent from "@testing-library/user-event";
import { Counter } from "./Counter";
test("increments on click", async () => {
const user = userEvent.setup();
render(<Counter />);
const button = screen.getByRole("button");
await user.click(button);
expect(button).toHaveTextContent("1");
});