Jesse A. Green / notebook

Live demo: the DocCompiler guide library as a static, read-only snapshot (October 2026). No backend, no accounts, nothing to submit.

Preact User Guide

Generated June 2026 from preactjs/preact at 850bbdb

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

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 main branch). 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 (the v10.x branch / preact@^10 on npm). Differences that matter to v10 users are flagged throughout with a "v11" note.


Table of Contents

  1. Quick Start
  2. Installation
  3. Core Concepts
  4. Rendering: render and hydrate
  5. Creating Elements: h / createElement and JSX
  6. Components
  7. Hooks (preact/hooks)
  8. Context
  9. Refs
  10. The React Compatibility Layer (preact/compat)
  11. Suspense and Lazy Loading
  12. Server-Side Rendering
  13. Debugging (preact/debug) and DevTools
  14. Testing (preact/test-utils)
  15. Common Tasks (Recipes)
  16. Troubleshooting
  17. FAQ
  18. 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, and options. Hooks live in a separate import (preact/hooks), and React-API extras live in preact/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's devDependencies (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 (via queueMicrotask). This is configurable through options.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/hooks and use class instead of className. Reach for preact/compat when 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() and hydrate() take exactly two arguments and return void. The optional third callback argument and the returned component reference exist only in preact/compat's render/hydrate. If you relied on render(vnode, parent, callback), import render from preact/compat or move the callback into an effect.

Caution

Calling render() on document itself is redirected to document.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 ref prop is only extracted as a ref when type is a DOM string. On function/class components, you pass ref through props and handle it yourself, or use forwardRef from preact/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, and context as arguments to render(), so you can destructure them directly: render({ title }, { open }) { ... }. You can still use this.props / this.state if 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 setState is passed a falsy update it returns without scheduling a render. State updates are batched — multiple setState calls 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/compat installed, the UNSAFE_-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: useLayoutEffect cleanup/run happens synchronously during commit; useEffect runs after paint (scheduled via requestAnimationFrame + a 35 ms setTimeout fallback 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, useContext returns the defaultValue you passed to createContext.

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 ref must be a function or an object created by createRef()/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; onChange on text <input>/<textarea> becomes oninput (fires on every keystroke); onDoubleClick → ondblclick; onFocus/onBlur → onfocusin/onfocusout; camelCased SVG attributes are dashed; numeric style values get a px suffix where applicable; defaultValue/defaultChecked are honored; download={true} renders as download=""; and array value on <select multiple> is supported. defaultProps on components are applied. Events also gain persist(), nativeEvent, isPropagationStopped(), and isDefaultPrevented().

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) require preact-render-to-string ≥ 6.5.0. If the dependency is missing or too old, you'll see an error like renderToReadableStream() 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 ref and 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).
  • NaN in a hook dependency array.
  • setState/forceUpdate called in a constructor or on an unmounted component.
  • Hydration mismatches between SSR and client markup.

Important

Never ship preact/debug to production — it adds size and runtime overhead. Strip it via your bundler's production conditionals (e.g. only import it when process.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 onInput for keystroke-level updates. With preact/compat, React's onChange on text inputs is automatically mapped to onInput.

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 forgotten export.
  • Solution: Verify the import/export. import Foo from './Foo' requires export default; import { Foo } requires export 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 ref was passed a value that isn't a function or createRef()/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}), or JSON.stringify(obj).
  • Prevention: Only render primitives, VNodes, or arrays of them.

Error: Missing Suspense. The throwing component was: <Name>

  • Cause: A lazy component 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() is null/undefined — usually document.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 render isn'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: setState was 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/setState ran 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 the useEffect cleanup).
  • 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 NaN value appeared in a hook's inputs array; 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-source to 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