Skip to main content

Overview

While preact/compat provides excellent React compatibility, there are some differences in behavior and implementation details. Most applications won’t encounter these, but it’s important to be aware of them.
In most cases, preact/compat provides 100% compatibility with React for standard usage patterns. Differences typically only matter for edge cases or advanced usage.

Core Differences

Synthetic Events

Preact uses native browser events instead of React’s synthetic event system. React:
function handleClick(e) {
  // e is a SyntheticEvent wrapper
  e.persist(); // Needed to access event async
  setTimeout(() => {
    console.log(e.type); // Works in React
  }, 100);
}
Preact:
function handleClick(e) {
  // e is a native Event
  // e.persist() is a no-op (see compat/src/render.js:100)
  setTimeout(() => {
    console.log(e.type); // Works in Preact
  }, 100);
}
The compat layer adds a no-op persist() method to events at compat/src/render.js:100 for compatibility, but it’s not needed since Preact uses native events.
Implications:
  • Native events are not pooled (no need for e.persist())
  • Event properties can be accessed asynchronously without issues
  • Slightly better performance due to no synthetic event overhead

Event Naming

Preact automatically normalizes React event names to their browser equivalents:
// These are automatically converted by the compat layer:
<input onChange={handler} />     // → oninput (for text inputs)
<input onDoubleClick={handler} /> // → ondblclick
<input onFocus={handler} />       // → onfocusin  
<input onBlur={handler} />        // → onfocusout
The conversion logic is in compat/src/render.js:168.
For <input type="file">, <input type="checkbox">, and <input type="radio">, onChange behaves exactly like React’s onChange, not as oninput.

className vs class

Preact supports both class and className props:
// Both work in Preact:
<div className="foo" />
<div class="foo" />
The compat layer ensures both are supported (see compat/src/render.js:227).

Lifecycle Methods

Unsafe Lifecycle Methods

Preact supports both prefixed and unprefixed unsafe lifecycle methods through property getters/setters:
class MyComponent extends Component {
  // Both of these work:
  componentWillMount() { }
  UNSAFE_componentWillMount() { }
  
  componentWillReceiveProps(nextProps) { }
  UNSAFE_componentWillReceiveProps(nextProps) { }
  
  componentWillUpdate(nextProps, nextState) { }
  UNSAFE_componentWillUpdate(nextProps, nextState) { }
}
The aliasing is implemented at compat/src/render.js:49.
While these lifecycle methods are supported, they’re deprecated in React and should be avoided in new code.

Concurrent Features

Transitions and Deferred Values

useTransition and useDeferredValue are implemented but don’t provide true concurrent rendering:
import { useTransition, useDeferredValue } from 'react';

function MyComponent() {
  const [isPending, startTransition] = useTransition();
  // isPending is always false
  // startTransition executes immediately
  
  const deferredValue = useDeferredValue(value);
  // Returns the value immediately (no deferral)
}
Implementation details:
Code that relies on actual concurrent rendering behavior won’t work the same way in Preact. These hooks are provided for API compatibility only.

flushSync

flushSync is implemented as a passthrough in Preact:
import { flushSync } from 'react-dom';

// In React: Forces synchronous render
// In Preact: Just executes the callback (compat/src/index.js:132)
flushSync(() => {
  setState(newValue);
});

Suspense and Lazy Loading

Suspense and lazy loading are fully supported:
import { lazy, Suspense } from 'react';

const LazyComponent = lazy(() => import('./Component'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <LazyComponent />
    </Suspense>
  );
}
The implementation at compat/src/suspense.js:218 provides full Suspense support including:
  • Error boundaries for promise rejection
  • Proper fallback rendering
  • Support for nested Suspense boundaries

Rendering Differences

Render Return Value

Preact’s render returns the component instance, matching React’s behavior:
import { render } from 'react-dom';

const instance = render(<App />, container);
// instance is the root component (or null for functional components)
This is ensured by the compat layer at compat/src/render.js:86.

Container Clearing

React clears container content on first render:
const container = document.getElementById('root');
container.innerHTML = '<div>This will be cleared</div>';

// First render clears the container
render(<App />, container);
Preact/compat implements the same behavior at compat/src/render.js:79.

Hydration

Hydration works similarly to React:
import { hydrateRoot } from 'react-dom/client';
// or legacy:
import { hydrate } from 'react-dom';

hydrate(<App />, container);

Props and Attributes

defaultValue and value

The compat layer handles defaultValue as a fallback for value:
// If value is null/undefined, defaultValue is used
<input value={null} defaultValue="fallback" />
// Renders with value="fallback"
Implementation: compat/src/render.js:152

SVG Attributes

Preact automatically converts camelCase SVG attributes to kebab-case:
// These are converted automatically:
<svg>
  <text textAnchor="middle" />      {/* → text-anchor */}
  <path strokeWidth={2} />           {/* → stroke-width */}
  <circle fillOpacity={0.5} />       {/* → fill-opacity */}
</svg>
The conversion regex is at compat/src/render.js:32.

Style Object

Numeric style values automatically get ‘px’ appended (except for unitless properties):
<div style={{ 
  width: 100,        // → "100px"
  height: 200,       // → "200px"
  flex: 1,           // → "1" (unitless)
  opacity: 0.5,      // → "0.5" (unitless)
  zIndex: 10         // → "10" (unitless)
}} />
Implementation: compat/src/render.js:148

Boolean Attributes

The download attribute with true value becomes an empty string:
// In React and Preact/compat:
<a href="/file" download={true} />
// Renders as: <a href="/file" download="">
This prevents the file from being named “true” (compat/src/render.js:159).

Component Features

PureComponent

PureComponent performs shallow comparison of props and state:
import { PureComponent } from 'react';

class MyComponent extends PureComponent {
  render() {
    // Only re-renders if props or state shallowly differ
    return <div>{this.props.value}</div>;
  }
}
The shallow comparison implementation is at compat/src/PureComponent.js:14 and uses the shallowDiffers utility from compat/src/util.js:9.

memo()

The memo higher-order component works identically to React:
import { memo } from 'react';

const MemoizedComponent = memo(MyComponent, (prevProps, nextProps) => {
  // Return true if props are equal (skip render)
  return prevProps.id === nextProps.id;
});
Implementation: compat/src/memo.js:11

forwardRef()

forwardRef is fully supported:
import { forwardRef } from 'react';

const MyInput = forwardRef((props, ref) => {
  return <input ref={ref} {...props} />;
});
The implementation at compat/src/forwardRef.js:12 ensures compatibility with libraries like mobx-react by:
  • Adding the $$typeof symbol
  • Exposing a render property
  • Setting isReactComponent flag

Children API

The Children API is fully compatible:
import { Children } from 'react';

function Parent({ children }) {
  const count = Children.count(children);
  const array = Children.toArray(children);
  
  Children.forEach(children, (child, index) => {
    console.log(child, index);
  });
  
  const mapped = Children.map(children, (child) => {
    return cloneElement(child, { key: child.key });
  });
  
  const only = Children.only(children); // Throws if not exactly one child
  
  return <div>{children}</div>;
}
Implementation: compat/src/Children.js:9

StrictMode

StrictMode is a no-op in Preact (it’s aliased to Fragment):
import { StrictMode } from 'react';

// In Preact, this just renders children without any checks
<StrictMode>
  <App />
</StrictMode>
See compat/src/index.js:151.
StrictMode development warnings and double-rendering don’t occur in Preact.

Utilities

isValidElement

Checks if a value is a valid Preact/React element:
import { isValidElement } from 'react';

const element = <div />;
isValidElement(element); // true
isValidElement('string'); // false
isValidElement(null); // false
Implementation checks for the $$typeof property at compat/src/index.js:58.

findDOMNode

Legacy API that’s supported but deprecated:
import { findDOMNode } from 'react-dom';

class MyComponent extends Component {
  componentDidMount() {
    const node = findDOMNode(this);
    // node is the DOM element
  }
}
findDOMNode is deprecated in React and may not work with all component types. Use refs instead.

unmountComponentAtNode

import { unmountComponentAtNode } from 'react-dom';

const container = document.getElementById('root');
render(<App />, container);

// Later:
const didUnmount = unmountComponentAtNode(container);
// didUnmount is true if a component was unmounted
Implementation: compat/src/index.js:101

Hooks Differences

useInsertionEffect

useInsertionEffect is aliased to useLayoutEffect in Preact:
import { useInsertionEffect } from 'react';

// In Preact, this behaves like useLayoutEffect
useInsertionEffect(() => {
  // Insert styles, etc.
}, []);
See compat/src/hooks.js:66.

useSyncExternalStore

useSyncExternalStore is fully implemented:
import { useSyncExternalStore } from 'react';

function useStore(store) {
  return useSyncExternalStore(
    store.subscribe,
    store.getSnapshot
  );
}
The implementation at compat/src/hooks.js:8 provides React-compatible behavior for external store subscriptions.

Server-Side Rendering

renderToString

Server rendering is fully supported:
import { renderToString } from 'react-dom/server';

const html = renderToString(<App />);
Preact’s SSR implementation is available through the compat layer.

Known Limitations

The following React features are not fully supported in Preact:

1. Concurrent Rendering

Preact doesn’t implement React’s concurrent rendering features:
  • Time slicing
  • Selective hydration
  • Priority-based rendering
Transition hooks are no-ops (see Concurrent Features section above).

2. Server Components

React Server Components (RSC) are not supported:
// Not supported in Preact:
'use server';
'use client';

3. Error Boundaries in Render

Some edge cases with error boundaries may differ from React.

4. Deep React Internals

Libraries that access React internals via __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED may not work:
// Preact provides a partial implementation at compat/src/render.js:303
import { __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED } from 'react';

// Some internals are provided for library compatibility,
// but not all React internals are available

5. Legacy Context API

The legacy context API (getChildContext) has limited support compared to the modern Context API.

Performance Differences

Bundle Size

Preact with compat is significantly smaller than React:
  • Preact core: ~3kb gzipped
  • Preact + compat: ~5-6kb gzipped
  • React + ReactDOM: ~40kb+ gzipped

Runtime Performance

Preact is generally faster than React due to:
  • No synthetic event system overhead
  • Simpler reconciliation algorithm
  • Smaller runtime means better parsing time
  • Direct DOM manipulation

Compatibility Overhead

The compat layer adds:
  • ~2-3kb to bundle size
  • Minimal runtime overhead for prop normalization
  • Event handling normalization
If you don’t need React compatibility, importing from preact and preact/hooks directly instead of using the compat layer will give you the smallest possible bundle.

Best Practices

  1. Use the compat layer for third-party libraries only: Import from preact directly in your own code when possible.
  2. Test thoroughly: While most code works identically, test your app’s critical paths.
  3. Check library compatibility: Before adopting a React library, verify it works with Preact (most do).
  4. Avoid React internals: Don’t rely on undocumented React internals.
  5. Use refs over findDOMNode: Modern ref patterns are better supported.
  6. Monitor bundle size: Use tools like webpack-bundle-analyzer to track your bundle.

When to Use Preact

Preact with compat is ideal when:
  • Bundle size is critical (mobile, slow networks)
  • You want React’s API with better performance
  • You’re building a new project and want React library compatibility
  • You’re migrating from React and want minimal code changes
Consider staying with React if:
  • You need concurrent rendering features
  • You’re using React Server Components
  • You have deep integrations with React internals
  • Your team is heavily invested in React-specific tooling

Next Steps

Compat Overview

Review all supported React features

Migration Guide

Migrate your React app to Preact

Build docs developers (and LLMs) love