Preact Options Hooks
Preact exposes a mutable
object (
import { options } from 'preact'
) that acts as a global hook system into the rendering pipeline. Any tool, library, or devtool can monkey-patch these hooks to observe or intercept every phase of the diff/commit cycle — no fibers, no DevTools protocol, no
.
How to chain hooks
Always save the previous value before overwriting so other plugins (and Preact's own debug addon) keep working:
ts
import { options } from 'preact';
const prev = options.diffed;
options.diffed = (vnode) => {
// your logic
prev?.(vnode);
};
To unhook, never restore the saved reference as other plugins might get lost. Instead, use a bail flag so the options chain stays intact:
ts
let active = true;
const prev = options.diffed;
options.diffed = (vnode) => {
if (active) {
// your logic
}
prev?.(vnode);
};
// To stop observing without breaking other plugins:
active = false;
Available hooks and their fire order
During a single render cycle the hooks fire in this order:
| Hook | Mangled name | Fires when | Signature |
|---|
| before-diff | | Just before a VNode starts diffing (called for every VNode, host and component). Good place to detect start of a commit batch. | |
| before-render | | Immediately before a component VNode's / function body executes. Best place to start a performance timer. | |
| diffed | | After a VNode (and all its children) have been diffed. The DOM is updated at this point. Best place to stop a timer and read the resulting DOM. | |
| commit | | After the entire tree's diff is done and the commit queue is about to flush (lifecycle methods / effects). Signals end of a commit batch. | (vnode: VNode, commitQueue: Component[]) => void
|
| unmount | | Before a VNode is removed from the tree. | |
| hook | | When a hook (useState, useEffect, etc.) is invoked inside a component. is an integer identifying the hook kind. | (component: Component, index: number, hookType: number) => void
|
Less common hooks
| Hook | Mangled name | Purpose |
|---|
| | Called when a VNode is created ( / JSX). Can mutate the vnode. |
| | Called before DOM events are processed. |
options.debounceRendering
| options.debounceRendering
| Called so renders can be batched, by default this is . |
Accessing internal VNode properties
Preact 10.x mangles internal properties. The key ones on a VNode:
| Property | Mangled | Type | Meaning |
|---|
| component | | | The class/function component instance |
| dom | | | First DOM node produced by this VNode |
| children | | | Child VNodes |
| parent | | | Parent VNode |
| flags | | | Internal diff flags / start offset |
| index | | | Index in parent's children array |
Specifically for
, they have a certain bit-wise meaning, where
means that the vnode is suspended and
means that the node is hydrating.
Accessing internal Component properties
| Property | Mangled | Type | Meaning |
|---|
| vnode | | | The VNode this component rendered |
| nextState | | | Pending state (before commit) |
| dirty | | | Whether component is queued for re-render |
| force | | | Whether was called |
| hooks | | { __: HookState[], __h: HookState[] } | null
| Hooks state container. is the ordered list; is pending effects. |
Each
has
(the current value) and
(deps array, if applicable).
Getting a component's display name
ts
function getDisplayName(vnode) {
const type = vnode.type;
if (typeof type === 'string') return null; // host element like 'div'
if (typeof type === 'function') {
return type.displayName || type.name || null;
}
return null;
}
Finding the DOM node for a component VNode
A component VNode's
may be
(fragments, Providers, etc.). Walk the child VNode list:
ts
function getDOMNode(vnode) {
if (vnode.__e instanceof Element) return vnode.__e;
for (const child of vnode.__k || []) {
if (child?.__e instanceof Element) return child.__e;
}
return null;
}
Detecting mount vs update
A component is mounting if there is no previous props snapshot / no
. The simplest approach: track whether you've seen the component instance before (e.g., via a WeakMap or a custom property on the
).
Commit batching
fires for every VNode in a diff pass. The first call marks the start of a commit.
fires once at the end with the full commit queue. This lets you implement
/
semantics.
Important notes
- These hooks are synchronous — they run inline during diffing. Keep them fast.
- fires for all VNodes (host elements too), not just components. Filter with
typeof vnode.type === 'function'
when you only care about components, for dom-nodes or null for text-nodes.
- The mangled names (, , , , , etc.) are stable across Preact 10.x and are the canonical way to access internals — Preact's own addons (, ) use them.
- fires bottom-up (children before parents). fires top-down.
- Always chain (save + call previous hook) to avoid breaking other addons.
Some examples of real-world usage: