Skip to main content

Overview

Migrating from React to Preact with the compat layer is straightforward and can often be done without changing any application code. This guide walks you through the process.
Most React applications can be migrated to Preact by simply configuring bundler aliases and updating dependencies.

Migration Process

1

Install Preact

Remove React dependencies and install Preact:
npm uninstall react react-dom
npm install preact
Or keep React as a peer dependency if you’re migrating a library:
npm install preact --save-dev
2

Update package.json

Update your package.json to reference Preact:
package.json
{
  "dependencies": {
    "preact": "^10.19.0"
  },
  "devDependencies": {
    // Your build tools...
  }
}
If you’re using TypeScript, you may also want to install @types/react and @types/react-dom as dev dependencies for better IDE support.
3

Configure Bundler Aliases

Configure your bundler to alias React imports to Preact. Choose your bundler:

Webpack

webpack.config.js
module.exports = {
  //...other config
  resolve: {
    alias: {
      'react': 'preact/compat',
      'react-dom/test-utils': 'preact/test-utils',
      'react-dom': 'preact/compat',
      'react/jsx-runtime': 'preact/jsx-runtime'
    }
  }
};

Vite

vite.config.js
import { defineConfig } from 'vite';
import preact from '@preact/preset-vite';

export default defineConfig({
  plugins: [preact()],
  // OR manually configure aliases:
  resolve: {
    alias: {
      'react': 'preact/compat',
      'react-dom/test-utils': 'preact/test-utils',
      'react-dom': 'preact/compat',
      'react/jsx-runtime': 'preact/jsx-runtime'
    }
  }
});
The @preact/preset-vite plugin automatically configures aliases and optimizations for Preact.

Rollup

rollup.config.js
import alias from '@rollup/plugin-alias';
import resolve from '@rollup/plugin-node-resolve';

export default {
  plugins: [
    alias({
      entries: [
        { find: 'react', replacement: 'preact/compat' },
        { find: 'react-dom/test-utils', replacement: 'preact/test-utils' },
        { find: 'react-dom', replacement: 'preact/compat' },
        { find: 'react/jsx-runtime', replacement: 'preact/jsx-runtime' }
      ]
    }),
    resolve()
  ]
};

Parcel

Add to your package.json:
package.json
{
  "alias": {
    "react": "preact/compat",
    "react-dom/test-utils": "preact/test-utils",
    "react-dom": "preact/compat",
    "react/jsx-runtime": "preact/jsx-runtime"
  }
}

esbuild

build.js
import { build } from 'esbuild';

build({
  entryPoints: ['src/index.js'],
  bundle: true,
  outfile: 'dist/bundle.js',
  alias: {
    'react': 'preact/compat',
    'react-dom': 'preact/compat',
    'react/jsx-runtime': 'preact/jsx-runtime'
  }
});
4

Update TypeScript Configuration (if applicable)

If you’re using TypeScript, update your tsconfig.json:
tsconfig.json
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "preact",
    "paths": {
      "react": ["./node_modules/preact/compat/"],
      "react-dom": ["./node_modules/preact/compat/"],
      "react/jsx-runtime": ["./node_modules/preact/jsx-runtime"]
    }
  }
}
The paths configuration helps TypeScript resolve the aliased imports correctly but doesn’t perform the actual aliasing. Your bundler configuration is still required.
5

Update Entry Point

If you’re using React 18’s createRoot API, no changes are needed. If you’re using the legacy ReactDOM.render, you can either:Option 1: Keep using the compat render function
index.jsx
import { render } from 'react-dom';
import App from './App';

// This works with preact/compat
render(<App />, document.getElementById('root'));
Option 2: Switch to React 18 API (recommended)
index.jsx
import { createRoot } from 'react-dom/client';
import App from './App';

const root = createRoot(document.getElementById('root'));
root.render(<App />);
Option 3: Use Preact’s render directly
index.jsx
import { render } from 'preact';
import App from './App';

render(<App />, document.getElementById('root'));
6

Test Your Application

Build and run your application to ensure everything works:
npm run build
npm run dev
Run your test suite:
npm test
Check your bundle size! You should see a significant reduction compared to React.
7

Fix Compatibility Issues (if any)

While most code works without changes, you may need to address:
  • Libraries that check for specific React internals
  • Code using deprecated lifecycle methods (though these are supported via compat/src/render.js:49)
  • Custom synthetic event handling
  • PropTypes validation (consider removing in production)
See the Differences page for details on specific edge cases.

Migrating Common Patterns

Class Components

Class components work without changes. The compat layer ensures full compatibility:
import { Component, PureComponent } from 'react';

// Both work exactly as in React
class MyComponent extends Component {
  render() {
    return <div>{this.props.children}</div>;
  }
}

class OptimizedComponent extends PureComponent {
  render() {
    return <div>{this.props.value}</div>;
  }
}
The PureComponent implementation in compat/src/PureComponent.js:14 performs shallow prop and state comparison just like React.

Hooks

All React hooks are supported:
import { 
  useState, 
  useEffect, 
  useContext,
  useMemo,
  useCallback,
  useRef,
  useTransition,
  useDeferredValue
} from 'react';

function MyComponent() {
  const [state, setState] = useState(0);
  const [isPending, startTransition] = useTransition();
  
  // All hooks work as expected
  useEffect(() => {
    console.log('Mounted');
  }, []);
  
  return <div>{state}</div>;
}

Context API

import { createContext, useContext } from 'react';

const ThemeContext = createContext('light');

function ThemedButton() {
  const theme = useContext(ThemeContext);
  return <button className={theme}>Click me</button>;
}

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <ThemedButton />
    </ThemeContext.Provider>
  );
}

Refs and forwardRef

Refs work identically to React. The forwardRef implementation in compat/src/forwardRef.js:12 ensures full compatibility:
import { forwardRef, useRef, useImperativeHandle } from 'react';

const FancyInput = forwardRef((props, ref) => {
  const inputRef = useRef();
  
  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current.focus()
  }));
  
  return <input ref={inputRef} {...props} />;
});

Memo and Lazy

import { memo, lazy, Suspense } from 'react';

// Memoized component
const ExpensiveComponent = memo(({ data }) => {
  return <div>{/* expensive render */}</div>;
});

// Lazy-loaded component
const LazyComponent = lazy(() => import('./LazyComponent'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <LazyComponent />
    </Suspense>
  );
}

Portals

Portals are fully supported via compat/src/portals.js:70:
import { createPortal } from 'react-dom';

function Modal({ children }) {
  return createPortal(
    children,
    document.getElementById('modal-root')
  );
}

Handling Third-Party Libraries

Most Libraries Work Automatically

Popular libraries that work out of the box:
  • React Router
  • Redux / Redux Toolkit
  • React Query / TanStack Query
  • Zustand
  • React Hook Form
  • Formik
  • Styled Components
  • Emotion
  • Material-UI (MUI)
  • Chakra UI
  • Ant Design

Libraries That May Need Adjustments

Some libraries may require configuration:
// Example: Configure a library to work with Preact
import { options } from 'preact';

// Some libraries check for React DevTools
if (typeof window !== 'undefined') {
  window.React = require('preact/compat');
}

Optimizing After Migration

Remove Compat for Core Code

Once migrated, consider importing from preact directly in your own code for smaller bundles:
// Instead of:
import { useState } from 'react';

// Use:
import { useState } from 'preact/hooks';
import { render } from 'preact';

Configure Aliases for Specific Libraries Only

You can be selective about what uses the compat layer:
webpack.config.js
module.exports = {
  resolve: {
    alias: {
      // Only alias for node_modules
      'react$': 'preact/compat',
      'react-dom$': 'preact/compat'
    }
  }
};

Troubleshooting

Ensure your bundler aliases are configured correctly and that you’ve installed preact:
npm install preact
Double-check that your bundler configuration is being loaded.
Install React types as dev dependencies:
npm install --save-dev @types/react @types/react-dom
Update your tsconfig.json with the paths configuration shown in Step 4.
Some libraries perform deep checks on React internals. Options:
  1. Check if there’s a Preact-specific version of the library
  2. Report the issue to the library maintainers
  3. Look for alternative libraries
  4. Implement a compatibility shim
Update your test configuration to use the same aliases:
jest.config.js
module.exports = {
  moduleNameMapper: {
    '^react$': 'preact/compat',
    '^react-dom$': 'preact/compat',
    '^react-dom/test-utils$': 'preact/test-utils',
    '^react/jsx-runtime$': 'preact/jsx-runtime'
  }
};

Next Steps

Learn the Differences

Understand key differences between React and Preact

Compat Overview

Review all supported React features

Build docs developers (and LLMs) love