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.
Bundle Size
Preact with compat is significantly smaller than React:
Preact core : ~3kb gzipped
Preact + compat : ~5-6kb gzipped
React + ReactDOM : ~40kb+ gzipped
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
Use the compat layer for third-party libraries only : Import from preact directly in your own code when possible.
Test thoroughly : While most code works identically, test your app’s critical paths.
Check library compatibility : Before adopting a React library, verify it works with Preact (most do).
Avoid React internals : Don’t rely on undocumented React internals.
Use refs over findDOMNode : Modern ref patterns are better supported.
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