Preact is a fast 4 kB alternative to React with the same modern API. It gives you the full power of Virtual DOM components — JSX, hooks, function and class components, Context, refs, Suspense, and server-side rendering — without the bundle-size overhead. Through the optional preact/compat alias it can also act as a drop-in replacement for React in existing applications.
This guide covers everything you need to build user interfaces with Preact: installation, the rendering model, every public function and hook, the React-compatibility layer, debugging, server-side rendering, and a thorough troubleshooting reference. All commands, function signatures, and error messages here are taken directly from the Preact source.
Important
This guide documents Preact v11.0.0-beta.1, the in-development next major release (the
mainbranch). The v11 line is a pre-release and its API surface may still change before a stable release. If you are maintaining a production app on the current stable line, you most likely want Preact 10 (thev10.xbranch /preact@^10on npm). Differences that matter to v10 users are flagged throughout with a "v11" note.
Table of Contents
- Quick Start
- Installation
- Core Concepts
- Rendering:
renderandhydrate - Creating Elements:
h/createElementand JSX - Components
- Hooks (
preact/hooks) - Context
- Refs
- The React Compatibility Layer (
preact/compat) - Suspense and Lazy Loading
- Server-Side Rendering
- Debugging (
preact/debug) and DevTools - Testing (
preact/test-utils) - Common Tasks (Recipes)
- Troubleshooting
- FAQ
- API Quick Reference
1. Quick Start
Install Preact and render your first component into the page.
npm install preact
import { render } from 'preact';
import { useState } from 'preact/hooks';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
render(<Counter />, document.getElementById('app'));
Count: 0
[ Increment ]
Each click re-renders the Counter and Preact applies the minimum number of DOM operations needed to reflect the new state.
Tip
Preact's public API is intentionally tiny. The core package exports only
render,hydrate,createElement/h,Fragment,Component,cloneElement,createContext,createRef,toChildArray,isValidElement, andoptions. Hooks live in a separate import (preact/hooks), and React-API extras live inpreact/compat. This split is what keeps the base bundle so small.
2. Installation
Requirements
| Requirement | Detail |
|---|---|
| Runtime | Any modern browser (Preact targets all modern browsers). A DOM is required for render. |
| Module formats | ESM (import), CommonJS (require), and UMD builds are all published. |
| Build tool | A JSX transform (Babel, esbuild, SWC, TypeScript, or Vite) is recommended but not required — you can also use h directly or HTM tagged templates with no build step. |
| TypeScript | Type definitions ship with the package (src/index.d.ts). No @types/preact needed. |
Note
Preact has zero runtime dependencies. Everything in
package.json'sdevDependencies(Babel, microbundle, Vitest, oxlint, etc.) is build/test tooling for contributing to Preact itself — none of it is installed into your application.
Install with your package manager
npm
npm install preact
pnpm
pnpm add preact
yarn
yarn add preact
The sub-package entry points
Preact ships several independent entry points. Import only what you use — each is tree-shakeable and separately bundled.
| Import path | What it provides |
|---|---|
preact |
Core: render, hydrate, h/createElement, Component, Fragment, createContext, createRef, cloneElement, toChildArray, isValidElement, options. |
preact/hooks |
All hooks: useState, useReducer, useEffect, useLayoutEffect, useRef, useMemo, useCallback, useContext, useImperativeHandle, useErrorBoundary, useId, useDebugValue. |
preact/compat |
React compatibility layer: adds memo, forwardRef, PureComponent, createPortal, Children, Suspense, lazy, and React-named hooks/types. Aliasable as react/react-dom. |
preact/jsx-runtime, preact/jsx-dev-runtime |
Babel/TypeScript "automatic" JSX runtime (jsx, jsxs, jsxDEV, Fragment). |
preact/debug |
Development warnings, prop-type checks, and DevTools wiring. Import once at the top of your app. |
preact/devtools |
Connects a production build to the Preact DevTools browser extension. |
preact/test-utils |
act, setupRerender, teardown for unit tests. |
preact/compat/server |
Server-side rendering (renderToString and streaming), backed by preact-render-to-string. |
preact/compat/scheduler, preact/compat/client |
React scheduler and react-dom/client compatibility shims. |
3. Core Concepts
Before the API reference, here are the ideas that make everything else fall into place.
Virtual DOM (VNode)
: A lightweight JavaScript object describing what the UI should look like. You never construct VNodes by hand — JSX (or h) creates them. Preact compares (diffs) the new VNode tree against the previous one and updates only what changed.
Component
: A function or class that returns a VNode tree. Function components are the default; class components extend Component and gain lifecycle methods.
Props
: Read-only inputs passed to a component, written as JSX attributes. children is a special prop holding nested content.
State
: Mutable data owned by a component. Updating state (via the useState setter or this.setState) schedules a re-render.
Reconciliation / diffing : The process of applying the difference between two VNode trees to the real DOM with as few operations as possible. Preact's diff algorithm is highly optimized and supports keyed lists, Fragments, and SVG/MathML namespaces.
Note
Preact renders asynchronously by default. Calls to
setState/ state setters are batched and flushed on a microtask (viaqueueMicrotask). This is configurable throughoptions.debounceRendering— see the Admin Guide for the scheduler internals.
Preact vs. React: what's different out of the box
| Topic | Plain preact |
With preact/compat |
|---|---|---|
| Attribute name | Use class (and for) |
className and class both work; htmlFor/for both work |
| DOM property casing | Standard DOM property names | React-style names are normalized (e.g. onDoubleClick → ondblclick) |
onChange on text inputs |
Fires like the native change event (on blur) |
Normalized to oninput (fires on every keystroke, like React) |
defaultProps |
Not applied by core | Applied |
forwardRef |
Not needed for DOM refs; available via compat | Available |
<Suspense> / lazy |
Available via preact/compat |
Available |
| String refs | Not supported | Not supported (use callback or object refs) |
Tip
If you're writing a new app, prefer plain
preact+preact/hooksand useclassinstead ofclassName. Reach forpreact/compatwhen you need React-only APIs or want to consume React-ecosystem libraries unchanged.
4. Rendering: render and hydrate
render(vnode, parent)
Renders a VNode tree into a DOM container, diffing against any previous tree rendered into the same container.
import { render, h } from 'preact';
render(
<main>
<h1>Hello</h1>
</main>,
document.body
);
// Call render() again on the same container to update in place.
render(
<main>
<h1>Hello World!</h1>
</main>,
document.body
);
// ^ updates only the <h1> text — a single DOM operation.
| Parameter | Type | Description |
|---|---|---|
vnode |
ComponentChild |
The element/component tree to render. |
parent |
ContainerNode |
A DOM element, document fragment, or shadow root to render into. |
Note
v11 change: core
render()andhydrate()take exactly two arguments and returnvoid. The optional thirdcallbackargument and the returned component reference exist only inpreact/compat'srender/hydrate. If you relied onrender(vnode, parent, callback), importrenderfrompreact/compator move the callback into an effect.
Caution
Calling
render()ondocumentitself is redirected todocument.documentElement. Prefer rendering into a dedicated container element (e.g.<div id="app">) you control.
hydrate(vnode, parent)
Attaches Preact to existing server-rendered HTML without re-creating the DOM. Use this on the client when the initial markup was produced by server-side rendering. Hydration attaches event listeners and component state to the existing nodes and skips creating elements that already match.
import { hydrate } from 'preact';
import App from './App';
hydrate(<App />, document.getElementById('app'));
Warning
The hydrated tree must produce the same DOM structure as the server-rendered HTML. A mismatch triggers a hydration error (see Troubleshooting). Avoid rendering values that differ between server and client (e.g.
Date.now(), random IDs) during the first render.
5. Creating Elements: h / createElement and JSX
JSX is compiled into calls to h (also exported as createElement). These are equivalent:
// JSX
const vnode = <a href="/" class="link">Home</a>;
// Compiled output (classic runtime)
import { h } from 'preact';
const vnode = h('a', { href: '/', class: 'link' }, 'Home');
createElement(type, props, ...children) (alias h)
| Parameter | Type | Description |
|---|---|---|
type |
string | Component | Function |
A tag name ('div'), a function component, or a class component. |
props |
object | null |
Attributes/props. key and ref are extracted specially. |
...children |
ComponentChildren |
Any number of child VNodes, strings, numbers, arrays, or null. |
Returns a VNode.
Note
In plain Preact, the
refprop is only extracted as a ref whentypeis a DOM string. On function/class components, you passrefthroughpropsand handle it yourself, or useforwardReffrompreact/compat.
Fragment
Group multiple children without adding an extra DOM node.
import { Fragment } from 'preact';
function List() {
return (
<Fragment>
<li>One</li>
<li>Two</li>
</Fragment>
);
}
// Or use the shorthand:
function List2() {
return (
<>
<li>One</li>
<li>Two</li>
</>
);
}
Configuring the JSX transform
Automatic runtime (recommended)
Babel @babel/preset-react or TypeScript tsconfig.json:
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "preact"
}
}
No per-file pragma needed; the compiler imports jsx/jsxs from preact/jsx-runtime automatically.
Classic runtime
Add a per-file pragma, or configure it globally:
/** @jsx h */
import { h } from 'preact';
TypeScript equivalent:
{
"compilerOptions": {
"jsx": "react",
"jsxFactory": "h",
"jsxFragmentFactory": "Fragment"
}
}
No build step (HTM)
import { h, render } from 'preact';
import htm from 'htm';
const html = htm.bind(h);
render(html`<h1>Hello, no transpiler!</h1>`, document.body);
isValidElement(vnode)
Returns true if the argument is a valid Preact VNode.
import { isValidElement, h } from 'preact';
isValidElement(<div />); // true
isValidElement('hello'); // false
toChildArray(children)
Flattens the children prop into a flat array, dropping null/undefined/booleans. Useful when you need to count or map over children.
import { toChildArray } from 'preact';
function Count(props) {
return <p>{toChildArray(props.children).length} children</p>;
}
cloneElement(vnode, props, ...children)
Returns a copy of a VNode with merged props and (optionally) replaced children. key and ref from the new props override the originals.
import { cloneElement, h } from 'preact';
const original = <button class="primary">Save</button>;
const disabled = cloneElement(original, { disabled: true });
6. Components
Function components
A function component is any function that returns a VNode tree. It receives props as its first argument.
function Welcome(props) {
return <h1>Hello, {props.name}!</h1>;
}
// Usage:
<Welcome name="Ada" />;
Function components hold state and side effects through hooks.
Class components
Class components extend Component and implement a render(props, state, context) method. They gain this.setState(), this.forceUpdate(), and lifecycle methods.
import { Component } from 'preact';
class Clock extends Component {
state = { time: Date.now() };
componentDidMount() {
this.timer = setInterval(() => this.setState({ time: Date.now() }), 1000);
}
componentWillUnmount() {
clearInterval(this.timer);
}
render(props, state) {
return <time>{new Date(state.time).toLocaleTimeString()}</time>;
}
}
Tip
Preact passes
props,state, andcontextas arguments torender(), so you can destructure them directly:render({ title }, { open }) { ... }. You can still usethis.props/this.stateif you prefer.
this.setState(update, callback?)
Merges update into the component's state and schedules an asynchronous re-render.
| Form | Behavior |
|---|---|
this.setState({ key: value }) |
Shallow-merges the object into current state. |
this.setState((prevState, props) => ({ ... })) |
Functional updater; receives a clone of current state and props. Use this when the next state depends on the previous state. |
this.setState({...}, () => {...}) |
The callback runs after the component re-renders. |
this.setState(prev => ({ count: prev.count + 1 }));
Note
If
setStateis passed a falsyupdateit returns without scheduling a render. State updates are batched — multiplesetStatecalls in the same tick result in a single re-render.
this.forceUpdate(callback?)
Forces a synchronous-path re-render, skipping shouldComponentUpdate. Use sparingly; prefer state-driven updates.
Lifecycle methods
| Method | When it runs |
|---|---|
componentWillMount() |
Before the first render. (Legacy — prefer the constructor or useEffect.) |
componentDidMount() |
After the component is first rendered into the DOM. |
componentWillReceiveProps(nextProps, nextContext) |
Before re-render due to new props. (Legacy.) |
shouldComponentUpdate(nextProps, nextState, nextContext) |
Return false to skip a re-render. |
componentWillUpdate(nextProps, nextState, nextContext) |
Before a re-render (not called on force update). |
getSnapshotBeforeUpdate(prevProps, prevState) |
Capture DOM info (e.g. scroll position) right before changes are applied; result is passed to componentDidUpdate. |
componentDidUpdate(prevProps, prevState, snapshot) |
After a re-render is committed. |
componentWillUnmount() |
Right before the component is removed. Clean up timers/subscriptions here. |
componentDidCatch(error, errorInfo) |
Catch errors thrown by descendants (error boundary). |
static getDerivedStateFromProps(props, state) |
Compute derived state from props before render. |
static getDerivedStateFromError(error) |
Update state in response to a descendant error (error boundary). |
Important
With
preact/compatinstalled, theUNSAFE_-prefixed aliases (UNSAFE_componentWillMount,UNSAFE_componentWillReceiveProps,UNSAFE_componentWillUpdate) are wired to their unprefixed counterparts for React compatibility. In plain Preact, use the unprefixed names.
Error boundaries
Catch rendering errors in a subtree and show a fallback:
import { Component } from 'preact';
class ErrorBoundary extends Component {
state = { error: null };
static getDerivedStateFromError(error) {
return { error };
}
componentDidCatch(error, errorInfo) {
console.error(error, errorInfo.componentStack);
}
render(props, state) {
return state.error
? <p>Something went wrong: {String(state.error)}</p>
: props.children;
}
}
For function components, use the useErrorBoundary hook instead.
7. Hooks (preact/hooks)
Hooks add state and lifecycle to function components. Import them from preact/hooks (not from preact).
Caution
Hooks must be called unconditionally, in the same order, on every render, and only from inside a component's render (or another hook). Calling a hook conditionally, in a loop, or outside render throws:
Hook can only be invoked from render methods.
useState(initialState)
Returns a [value, setValue] pair.
import { useState } from 'preact/hooks';
const [name, setName] = useState('');
const [count, setCount] = useState(() => expensiveInitial()); // lazy init
setCount(c => c + 1); // functional update
The initial value may be a value or a function (called once for lazy initialization). The setter accepts a new value or an updater function. Preact bails out of re-rendering when the new value is Object.is-equal to the current one.
useReducer(reducer, initialState, init?)
A useState alternative for complex state logic.
import { useReducer } from 'preact/hooks';
function reducer(state, action) {
switch (action.type) {
case 'inc': return { count: state.count + 1 };
case 'dec': return { count: state.count - 1 };
default: return state;
}
}
const [state, dispatch] = useReducer(reducer, { count: 0 });
dispatch({ type: 'inc' });
The optional third argument init(initialArg) lazily computes the initial state.
useEffect(effect, inputs?)
Runs a side effect after the browser paints. Return a cleanup function to run before the next effect or on unmount. The effect re-runs only when a value in the inputs array changes (compared with Object.is).
import { useEffect } from 'preact/hooks';
useEffect(() => {
const id = setInterval(tick, 1000);
return () => clearInterval(id); // cleanup
}, []); // [] = run once on mount
inputs |
Behavior |
|---|---|
| omitted | Effect runs after every render. |
[] |
Effect runs once after mount; cleanup on unmount. |
[a, b] |
Effect re-runs whenever a or b changes. |
useLayoutEffect(effect, inputs?)
Same signature as useEffect, but runs synchronously after DOM mutations and before the browser paints. Use it to read layout (sizes, scroll) and apply synchronous visual changes without flicker. Prefer useEffect when you don't need synchronous timing.
useRef(initialValue)
Returns a mutable { current } object that persists across renders. Mutating .current does not trigger a re-render. Useful for DOM references and for storing any mutable value.
import { useRef } from 'preact/hooks';
function TextInput() {
const inputRef = useRef(null);
return <input ref={inputRef} />;
}
useImperativeHandle(ref, createHandle, inputs?)
Customize the value exposed to a parent's ref. Typically paired with forwardRef.
useImperativeHandle(ref, () => ({
focus: () => inputRef.current.focus()
}), []);
useMemo(factory, inputs)
Memoizes the result of factory(), recomputing only when inputs change.
import { useMemo } from 'preact/hooks';
const sorted = useMemo(() => items.slice().sort(compare), [items]);
useCallback(callback, inputs)
Returns a memoized version of callback that changes only when inputs change. Equivalent to useMemo(() => callback, inputs). Useful for stable function identity when passing callbacks to memoized children.
const onClick = useCallback(() => doThing(id), [id]);
useContext(context)
Reads the current value of a Context (see Context). Re-renders the component when the nearest provider's value changes.
import { useContext } from 'preact/hooks';
const theme = useContext(ThemeContext);
useErrorBoundary(callback?)
Function-component error boundary. Returns [error, resetError]. The optional callback(error, errorInfo) runs when a descendant throws.
import { useErrorBoundary } from 'preact/hooks';
function App() {
const [error, resetError] = useErrorBoundary();
if (error) {
return <button onClick={resetError}>Try again</button>;
}
return <Child />;
}
useId()
Returns a stable, unique string ID that is consistent between server and client renders — ideal for linking form labels and inputs accessibly.
import { useId } from 'preact/hooks';
function Field() {
const id = useId();
return (
<>
<label for={id}>Email</label>
<input id={id} type="email" />
</>
);
}
useDebugValue(value, formatter?)
Displays a label for a custom hook in the Preact DevTools. No-op when DevTools is not attached.
useDebugValue(isOnline ? 'Online' : 'Offline');
Tip
Hook execution timing at a glance:
useLayoutEffectcleanup/run happens synchronously during commit;useEffectruns after paint (scheduled viarequestAnimationFrame+ a 35 mssetTimeoutfallback so effects still fire in background tabs).
8. Context
Context passes data through the component tree without threading props at every level.
createContext(defaultValue)
import { createContext } from 'preact/hooks';
import { createContext as createCtx } from 'preact';
const ThemeContext = createCtx('light');
Provide a value with the context itself (or its .Provider), and consume it with useContext or <Context.Consumer>:
import { createContext } from 'preact';
import { useContext } from 'preact/hooks';
const ThemeContext = createContext('light');
function App() {
return (
<ThemeContext value="dark">
<Toolbar />
</ThemeContext>
);
}
function Toolbar() {
const theme = useContext(ThemeContext);
return <div class={theme}>Current theme: {theme}</div>;
}
Note
v11: a context object is itself a provider — you can render
<ThemeContext value={...}>directly. The classic<ThemeContext.Provider>and<ThemeContext.Consumer>forms remain available for compatibility. When no provider is present in the tree,useContextreturns thedefaultValueyou passed tocreateContext.
The render-prop Consumer form:
<ThemeContext.Consumer>
{value => <span>{value}</span>}
</ThemeContext.Consumer>
9. Refs
A ref gives you direct access to a DOM node or class-component instance.
createRef()
Creates a { current: null } ref object (for class components).
import { Component, createRef } from 'preact';
class Form extends Component {
input = createRef();
componentDidMount() {
this.input.current.focus();
}
render() {
return <input ref={this.input} />;
}
}
Callback refs
Pass a function to ref; it receives the node (or null on unmount):
<input ref={node => (this.input = node)} />
Ref hook (function components)
Use useRef in function components. For DOM refs, no forwardRef is needed when forwarding to a DOM element directly — but to forward a ref through a function component to a child, use forwardRef from preact/compat.
Warning
A
refmust be a function or an object created bycreateRef()/useRef(). Passing any other value throws in development:Component's "ref" property should be a function, or an object created by createRef(), but got [type] instead.
10. The React Compatibility Layer (preact/compat)
preact/compat makes Preact behave like React, so you can run React components and consume React-ecosystem libraries. It re-exports everything from preact and preact/hooks, plus the React-only APIs below. It reports version as "18.3.1" so libraries that sniff the React version behave correctly.
Using compat as a React drop-in
Alias react and react-dom to preact/compat in your bundler so existing React code resolves to Preact.
Vite / Rollup (vite.config.js)
export default {
resolve: {
alias: {
react: 'preact/compat',
'react-dom': 'preact/compat',
'react/jsx-runtime': 'preact/jsx-runtime',
'react-dom/test-utils': 'preact/test-utils'
}
}
};
Webpack (webpack.config.js)
module.exports = {
resolve: {
alias: {
react: 'preact/compat',
'react-dom': 'preact/compat',
'react/jsx-runtime': 'preact/jsx-runtime'
}
}
};
TypeScript (tsconfig.json)
{
"compilerOptions": {
"paths": {
"react": ["./node_modules/preact/compat/"],
"react-dom": ["./node_modules/preact/compat/"]
}
}
}
What compat adds
| API | Purpose |
|---|---|
memo(Component, areEqual?) |
Skip re-render when props are shallow-equal (or per a custom comparer). |
forwardRef(fn) |
Forward a ref through a function component to a child. |
PureComponent |
Class component with a built-in shallow-compare shouldComponentUpdate. |
createPortal(vnode, container) |
Render children into a different DOM node. |
Children |
map, forEach, count, only, toArray helpers over props.children. |
Suspense, lazy |
Code-splitting and async boundaries (see next section). |
StrictMode |
Alias of Fragment (a no-op wrapper). |
createFactory(type) |
Legacy element factory. |
isValidElement, isFragment, isMemo, isElement |
Element type guards. |
findDOMNode(component) |
Get the DOM node for a component (legacy). |
unmountComponentAtNode(container) |
Tear down a tree (legacy). |
flushSync(fn, arg?) |
Run fn and flush all pending updates synchronously. |
unstable_batchedUpdates(fn, arg?) |
Legacy batching shim (Preact already batches; this just calls fn). |
startTransition, useTransition, useDeferredValue |
React 18 concurrency shims (synchronous in Preact). |
useSyncExternalStore |
Subscribe to an external store (React 18 API). |
useInsertionEffect |
Aliased to useLayoutEffect. |
Note
DOM prop normalization in compat. When compat is active, vnodes are normalized to React conventions:
className⇄class;onChangeon text<input>/<textarea>becomesoninput(fires on every keystroke);onDoubleClick→ondblclick;onFocus/onBlur→onfocusin/onfocusout; camelCased SVG attributes are dashed; numericstylevalues get apxsuffix where applicable;defaultValue/defaultCheckedare honored;download={true}renders asdownload=""; and arrayvalueon<select multiple>is supported.defaultPropson components are applied. Events also gainpersist(),nativeEvent,isPropagationStopped(), andisDefaultPrevented().
memo
import { memo } from 'preact/compat';
const Row = memo(function Row({ label }) {
return <li>{label}</li>;
});
// Custom comparison:
const Row2 = memo(Row, (prev, next) => prev.id === next.id);
forwardRef
import { forwardRef } from 'preact/compat';
const FancyInput = forwardRef(function FancyInput(props, ref) {
return <input ref={ref} class="fancy" {...props} />;
});
createPortal
import { createPortal } from 'preact/compat';
function Modal({ children }) {
return createPortal(
<div class="modal">{children}</div>,
document.getElementById('modal-root')
);
}
11. Suspense and Lazy Loading
Suspense and lazy (from preact/compat) enable code-splitting and async boundaries. lazy wraps a dynamic import(); Suspense shows a fallback while the lazy component (or any descendant that "suspends" by throwing a promise) loads.
import { Suspense, lazy } from 'preact/compat';
const Profile = lazy(() => import('./Profile'));
function App() {
return (
<Suspense fallback={<div>Loading…</div>}>
<Profile />
</Suspense>
);
}
| API | Signature | Notes |
|---|---|---|
lazy(loader) |
loader: () => Promise<{ default: C } | C> |
Returns a component that throws the load promise until resolved, then renders the default export. |
Suspense |
props: { fallback, children } |
Renders fallback while any child is suspended, then swaps in the children. |
Warning
A component that suspends (throws a promise) must have a
<Suspense>ancestor. Without one, you get:Missing Suspense. The throwing component was: <Name>. Suspense boundaries can be nested to provide granular loading states.
Tip
Suspense also works with hydration: server-rendered markup inside a
<Suspense>is kept on screen and hydrated when the lazy content resolves, rather than being replaced by the fallback.
12. Server-Side Rendering
Preact renders to an HTML string on the server with the separate preact-render-to-string package, surfaced through preact/compat/server.
npm install preact-render-to-string
import { renderToString } from 'preact/compat/server';
import App from './App';
const html = renderToString(<App />);
// Send `html` to the browser, then call hydrate() on the client.
Export (preact/compat/server) |
Purpose |
|---|---|
renderToString(vnode) |
Render a tree to a complete HTML string. |
renderToStaticMarkup(vnode) |
Alias of renderToString (no extra hydration attributes). |
renderToPipeableStream(vnode) |
Node.js streaming SSR. |
renderToReadableStream(vnode) |
Web-streams SSR. |
Caution
The streaming functions (
renderToPipeableStream,renderToReadableStream) requirepreact-render-to-string≥ 6.5.0. If the dependency is missing or too old, you'll see an error likerenderToReadableStream() error: update "preact-render-to-string" dependency to at least 6.5.0.
On the client, finish by hydrating the server markup:
import { hydrate } from 'preact';
hydrate(<App />, document.getElementById('app'));
13. Debugging (preact/debug) and DevTools
preact/debug
Import preact/debug once, before any other Preact import, in development builds. It installs runtime validation that surfaces common mistakes as clear console errors and warnings, and connects to the DevTools extension.
// At the very top of your app entry (development only):
import 'preact/debug';
What it checks (a selection):
- Undefined/invalid components passed to
createElement. - Invalid
refand event-handler prop types. - Rendering objects as children.
- Duplicate
keys among siblings. - Improper HTML nesting (e.g.
<td>outside<tr>, block elements inside<p>, nested<a>/<button>). - Hooks called outside render; too many re-renders (infinite loop guard at 25).
NaNin a hook dependency array.setState/forceUpdatecalled in a constructor or on an unmounted component.- Hydration mismatches between SSR and client markup.
Important
Never ship
preact/debugto production — it adds size and runtime overhead. Strip it via your bundler's production conditionals (e.g. only import it whenprocess.env.NODE_ENV !== 'production').
preact/debug also re-exports stack helpers: captureOwnerStack(), getCurrentVNode(), getDisplayName(vnode), getOwnerStack(vnode), setupComponentStack(), and resetPropWarnings().
preact/devtools
To use the Preact DevTools browser extension in a production build (where preact/debug is absent), import preact/devtools:
import 'preact/devtools';
It also exports addHookName(value, name) to label custom hooks in the DevTools panel.
14. Testing (preact/test-utils)
preact/test-utils helps you write deterministic tests by flushing Preact's async renders and effects.
| Function | Purpose |
|---|---|
act(callback) |
Run callback, then flush all pending re-renders and effects. Returns a Promise. Supports nesting and async callbacks. |
setupRerender() |
Returns a function that synchronously drains the pending render queue. |
teardown() |
Reset Preact's internal scheduling state between tests. |
import { render } from 'preact';
import { act } from 'preact/test-utils';
await act(() => {
render(<Counter />, scratch);
});
// State updates and effects have now flushed; assert on the DOM.
Tip
Wrap any code that triggers state updates in
act()so effects and re-renders complete before your assertions run.
15. Common Tasks (Recipes)
Conditional rendering
function Greeting({ user }) {
return user ? <p>Welcome, {user.name}</p> : <p>Please sign in</p>;
}
Rendering lists with keys
function TodoList({ items }) {
return (
<ul>
{items.map(item => (
<li key={item.id}>{item.text}</li>
))}
</ul>
);
}
Warning
Always give list items a stable, unique
key. Duplicate keys among siblings trigger a console error and can cause incorrect DOM reuse during reordering.
Controlled form input
import { useState } from 'preact/hooks';
function NameForm() {
const [name, setName] = useState('');
return (
<input value={name} onInput={e => setName(e.currentTarget.value)} />
);
}
Note
In plain Preact use
onInputfor keystroke-level updates. Withpreact/compat, React'sonChangeon text inputs is automatically mapped toonInput.
Inline styles
<div style={{ color: 'red', marginTop: 8 }}>Styled</div>
In preact/compat, numeric values for dimensional properties (e.g. marginTop: 8) are suffixed with px automatically.
Lazy-loaded route
import { Suspense, lazy } from 'preact/compat';
const Settings = lazy(() => import('./routes/Settings'));
<Suspense fallback={<Spinner />}>
<Settings />
</Suspense>;
Custom hook
import { useState, useEffect } from 'preact/hooks';
function useOnline() {
const [online, setOnline] = useState(navigator.onLine);
useEffect(() => {
const on = () => setOnline(true);
const off = () => setOnline(false);
addEventListener('online', on);
addEventListener('offline', off);
return () => {
removeEventListener('online', on);
removeEventListener('offline', off);
};
}, []);
return online;
}
16. Troubleshooting
These are real validation messages emitted by preact/debug, plus common runtime issues. Each entry lists the cause, fix, and how to prevent it.
Error: Hook can only be invoked from render methods.
- Cause: A hook (
useState,useEffect, etc.) was called outside a component's render — e.g. in an event handler, at module top level, conditionally, or in a loop. - Solution: Move the hook to the top level of a function component or custom hook. Call hooks unconditionally and in the same order every render.
- Prevention: Follow the Rules of Hooks; use an ESLint hooks plugin.
Error: Undefined component passed to createElement()
- Cause: A component used in JSX is
undefined— usually a wrong import (mixing default and named imports) or a forgottenexport. - Solution: Verify the import/export.
import Foo from './Foo'requiresexport default;import { Foo }requiresexport function Foo. - Prevention: Keep import style consistent with the export; let TypeScript catch missing exports.
Error: Invalid type passed to createElement(): ... Did you accidentally pass a JSX literal as JSX twice?
- Cause: A VNode (the result of evaluating JSX) was used as a component type, e.g.
export default <App />then<MyApp />. - Solution: Export the component itself (
export default App), not a rendered element. - Prevention: Don't store JSX literals where a component is expected.
Error: Component's "ref" property should be a function, or an object created by createRef(), but got [<type>] instead
- Cause: A
refwas passed a value that isn't a function orcreateRef()/useRef()object. - Solution: Use a callback ref or a ref object. String refs are not supported.
- Prevention: Create refs with
useRef()/createRef().
Error: Component's "<onX>" property should be a function, but got [<type>] instead
- Cause: An event handler prop (e.g.
onClick) was given a non-function value. - Solution: Pass a function:
onClick={() => handle()}. - Prevention: Avoid calling the handler inline (
onClick={handle()}calls it immediately and assigns the result).
Error: Too many re-renders. This is limited to prevent an infinite loop which may lock up your browser. The component causing this is: <Name>
- Cause: A component updates its own state during render (≥ 25 renders in a row for the same component), creating a loop.
- Solution: Move state updates into event handlers or effects, not the render body.
- Prevention: Never call a state setter unconditionally during render.
Error: Objects are not valid as a child. Encountered an object with the keys {<keys>}.
- Cause: A plain object was rendered as a child, e.g.
<div>{someObject}</div>. - Solution: Render a string/number, or pick specific fields (
{obj.name}), orJSON.stringify(obj). - Prevention: Only render primitives, VNodes, or arrays of them.
Error: Missing Suspense. The throwing component was: <Name>
- Cause: A
lazycomponent or other suspending component has no<Suspense>ancestor. - Solution: Wrap it in
<Suspense fallback={…}>. - Prevention: Always pair
lazy()/suspense-based data loading with a boundary.
Error: Undefined parent passed to render(), this is the second argument.
- Cause: The container passed to
render()/hydrate()isnull/undefined— usuallydocument.getElementById('app')returned nothing. - Solution: Ensure the container exists in the DOM and the id matches; run after the DOM is ready.
- Prevention: Render into a known container; guard against a missing element.
Error: Expected a valid HTML node as a second argument to render. Received <x> instead
- Cause: The second argument to
renderisn't an Element, Document, or DocumentFragment. - Solution: Pass a real DOM container node.
- Prevention: Don't pass strings, components, or arbitrary objects as the container.
Warning: Following component has two or more children with the same key attribute: "<key>"
- Cause: Sibling list items share a
key. - Solution: Make keys unique within the list (use a stable id, not the array index when items reorder).
- Prevention: Derive keys from unique data.
Error: Expected a DOM node of type "<type>" but found "<other>" ... caused by the SSR'd HTML containing different DOM-nodes compared to the hydrated one.
- Cause: Hydration mismatch — the client tree differs from the server markup.
- Solution: Ensure server and client render identically; avoid non-deterministic values (
Date.now(),Math.random()) during the first render; move them into effects. - Prevention: Render the same output on both sides; use
useId()for IDs.
Warning: Calling "this.setState" inside the constructor of a component is a no-op...
- Cause:
setStatewas called in the constructor. - Solution: Set
this.state = { ... }directly in the constructor instead. - Prevention: Initialize state with a class field or in the constructor by assignment.
Warning: Can't call "this.forceUpdate" on an unmounted component...
- Cause:
forceUpdate/setStateran after the component unmounted — often an async callback or subscription that wasn't cleaned up. - Solution: Cancel timers, subscriptions, and pending promises in
componentWillUnmount(or theuseEffectcleanup). - Prevention: Always return a cleanup function from effects.
Warning: Invalid argument passed to hook. Hooks should not be called with NaN in the dependency array.
- Cause: A
NaNvalue appeared in a hook'sinputsarray;NaN !== NaN, so the effect would run every render. - Solution: Ensure dependency values are well-defined numbers; guard computations that can produce
NaN. - Prevention: Validate numeric dependencies.
Warning: Add @babel/plugin-transform-react-jsx-source to get a more detailed component stack.
- Cause: Component stacks lack file/line info because the JSX source plugin isn't in your dev toolchain.
- Solution: Add
@babel/plugin-transform-react-jsx-sourceto development builds only. - Prevention: Keep it out of production builds (it increases bundle size).
17. FAQ
Q: Is Preact really compatible with React?
A: The core API (components, hooks, JSX, context, refs) matches React's. For React-only APIs (memo, forwardRef, Suspense, portals, etc.) and ecosystem libraries, alias react/react-dom to preact/compat.
Q: Do I have to use JSX?
A: No. You can call h/createElement directly, or use HTM tagged templates for a build-free workflow.
Q: Should I use class or className?
A: In plain Preact, use class. With preact/compat, both work and are normalized.
Q: Why doesn't my text input update on every keystroke with onChange?
A: Plain Preact's onChange mirrors the native change event (fires on blur). Use onInput for per-keystroke updates, or use preact/compat, which maps onChange to oninput for text inputs.
Q: Where do hooks come from?
A: Import them from preact/hooks, not preact. This keeps the core bundle small.
Q: How big is Preact?
A: The core is roughly 4 kB gzipped — see the live badges in the repository README. preact/hooks and preact/compat add small amounts on top, and everything is tree-shakeable.
Q: Does Preact have runtime dependencies?
A: No. The core has zero runtime dependencies. SSR uses the separate preact-render-to-string package.
Q: Are setState updates synchronous?
A: No — they're batched and flushed asynchronously on a microtask. Use the setState callback, useEffect, or flushSync (compat) if you need to act after the DOM updates.
Q: How do I render into a different part of the DOM (e.g. a modal)?
A: Use createPortal from preact/compat.
Q: How do I get a ref to a child function component's DOM node?
A: Wrap the child with forwardRef from preact/compat, or expose an imperative API with useImperativeHandle.
Q: What's the difference between useEffect and useLayoutEffect?
A: useEffect runs after paint (non-blocking). useLayoutEffect runs synchronously after DOM mutations but before paint — use it to measure layout or prevent visual flicker.
Q: Can I use Preact with TypeScript?
A: Yes — types ship with the package. Set jsxImportSource: "preact" (automatic runtime) or jsxFactory: "h" (classic runtime).
Q: How do I enable the DevTools extension?
A: Import preact/debug in development, or preact/devtools in production builds.
Q: Should I use the v11 beta or v10?
A: For production, use stable v10 (preact@^10). Use v11 beta only to test the upcoming release or for greenfield projects that can tolerate pre-release churn.
18. API Quick Reference
preact (core)
| Export | Signature | Description |
|---|---|---|
render |
render(vnode, parent) |
Render/update a tree into a DOM container. |
hydrate |
hydrate(vnode, parent) |
Attach to existing SSR markup. |
h / createElement |
(type, props, ...children) → VNode |
Create a virtual node. |
Fragment |
component | Group children without a wrapper node. |
Component |
class | Base class for class components. |
cloneElement |
(vnode, props, ...children) → VNode |
Copy a VNode with merged props. |
createContext |
(defaultValue) → Context |
Create a context. |
createRef |
() → { current: null } |
Create a ref object. |
toChildArray |
(children) → array |
Flatten children. |
isValidElement |
(value) → boolean |
VNode type guard. |
options |
object | Renderer hook registry (advanced — see Admin Guide). |
preact/hooks
| Hook | Signature |
|---|---|
useState |
(initial) → [value, setValue] |
useReducer |
(reducer, initial, init?) → [state, dispatch] |
useEffect |
(effect, inputs?) → void |
useLayoutEffect |
(effect, inputs?) → void |
useRef |
(initial) → { current } |
useImperativeHandle |
(ref, createHandle, inputs?) → void |
useMemo |
(factory, inputs) → value |
useCallback |
(callback, inputs) → callback |
useContext |
(context) → value |
useErrorBoundary |
(callback?) → [error, reset] |
useId |
() → string |
useDebugValue |
(value, formatter?) → void |
preact/compat (selected)
| Export | Signature |
|---|---|
memo |
(Component, areEqual?) → Component |
forwardRef |
(fn) → Component |
PureComponent |
class |
createPortal |
(vnode, container) → VNode |
Suspense |
component ({ fallback, children }) |
lazy |
(loader) → Component |
Children |
{ map, forEach, count, only, toArray } |
flushSync |
(fn, arg?) → result |
startTransition / useTransition / useDeferredValue |
React 18 concurrency shims |
useSyncExternalStore |
(subscribe, getSnapshot) → value |