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:
Preact:
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:
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:
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:
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:
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:

Suspense and Lazy Loading

Suspense and lazy loading are fully supported:
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:
This is ensured by the compat layer at compat/src/render.js:86.

Container Clearing

React clears container content on first render:
Preact/compat implements the same behavior at compat/src/render.js:79.

Hydration

Hydration works similarly to React:

Props and Attributes

defaultValue and value

The compat layer handles defaultValue as a fallback for value:
Implementation: compat/src/render.js:152

SVG Attributes

Preact automatically converts camelCase SVG attributes to kebab-case:
The conversion regex is at compat/src/render.js:32.

Style Object

Numeric style values automatically get ‘px’ appended (except for unitless properties):
Implementation: compat/src/render.js:148

Boolean Attributes

The download attribute with true value becomes an empty string:
This prevents the file from being named “true” (compat/src/render.js:159).

Component Features

PureComponent

PureComponent performs shallow comparison of props and state:
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:
Implementation: compat/src/memo.js:11

forwardRef()

forwardRef is fully supported:
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:
Implementation: compat/src/Children.js:9

StrictMode

StrictMode is a no-op in Preact (it’s aliased to Fragment):
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:
Implementation checks for the $$typeof property at compat/src/index.js:58.

findDOMNode

Legacy API that’s supported but deprecated:
findDOMNode is deprecated in React and may not work with all component types. Use refs instead.

unmountComponentAtNode

Implementation: compat/src/index.js:101

Hooks Differences

useInsertionEffect

useInsertionEffect is aliased to useLayoutEffect in Preact:
See compat/src/hooks.js:66.

useSyncExternalStore

useSyncExternalStore is fully implemented:
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:
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:

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:

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