Skip to main content

Options API

The Options API is Preact’s extension mechanism that allows you to hook into the rendering lifecycle and customize its behavior. This is the foundation for addons like preact/debug, preact/compat, and preact/hooks.
The Options API is primarily for library authors and advanced use cases. Most application developers won’t need to use it directly.

Overview

The options object contains callback functions that Preact invokes at various stages of the rendering process:
import { options } from 'preact';

// Hook into the rendering lifecycle
options.vnode = (vnode) => {
  console.log('VNode created:', vnode);
};
Source: src/options.js:1-17

Public Options Hooks

These hooks are part of Preact’s public API and safe to use:

vnode

Invoked whenever a VNode is created, before it’s processed:
options.vnode = (vnode) => {
  // Modify or inspect vnodes as they're created
  console.log('Creating:', vnode.type);
};
Type: (vnode: VNode) => void Use cases:
  • Inject additional props
  • Track component creation
  • Implement custom prop transformations
  • Add metadata to vnodes
Source: src/index.d.ts:329 Example - Auto-inject props:
options.vnode = (vnode) => {
  if (typeof vnode.type === 'function') {
    // Add a debug ID to every component
    vnode.props = {
      ...vnode.props,
      __debugId: Math.random().toString(36)
    };
  }
};

diffed

Invoked after a vnode has been diffed and rendered:
options.diffed = (vnode) => {
  // Called after component renders
  console.log('Rendered:', vnode.type);
};
Type: (vnode: VNode) => void Use cases:
  • Track render completion
  • Measure render performance
  • Update external state after render
  • Implement custom side effects
Source: src/index.d.ts:333 Example - Performance monitoring:
const renderTimes = new Map();

options.vnode = (vnode) => {
  if (typeof vnode.type === 'function') {
    vnode.startTime = performance.now();
  }
};

options.diffed = (vnode) => {
  if (vnode.startTime) {
    const duration = performance.now() - vnode.startTime;
    const name = vnode.type.displayName || vnode.type.name;
    console.log(`${name} rendered in ${duration.toFixed(2)}ms`);
  }
};

unmount

Invoked immediately before a vnode is unmounted:
options.unmount = (vnode) => {
  // Cleanup before unmount
  console.log('Unmounting:', vnode.type);
};
Type: (vnode: VNode) => void Use cases:
  • Custom cleanup logic
  • Track component lifecycle
  • Remove external references
  • Clear caches or subscriptions
Source: src/index.d.ts:331 Example - Resource cleanup:
const activeResources = new WeakMap();

options.vnode = (vnode) => {
  if (vnode.type === 'img') {
    const resource = { src: vnode.props.src };
    activeResources.set(vnode, resource);
  }
};

options.unmount = (vnode) => {
  const resource = activeResources.get(vnode);
  if (resource) {
    console.log('Cleaning up resource:', resource.src);
    activeResources.delete(vnode);
  }
};

event

Transform or handle events before they’re dispatched:
options.event = (event) => {
  // Modify event before dispatch
  console.log('Event:', event.type);
  return event;
};
Type: (event: Event) => any Use cases:
  • Event normalization
  • Analytics tracking
  • Custom event handling
  • Polyfill event properties
Source: src/index.d.ts:334 Example - Event analytics:
options.event = (event) => {
  if (event.type === 'click') {
    analytics.track('click', {
      target: event.target.tagName,
      timestamp: Date.now()
    });
  }
  return event;
};

requestAnimationFrame

Customize how Preact schedules effects:
options.requestAnimationFrame = (callback) => {
  // Custom scheduling
  setTimeout(callback, 16);
};
Type: (callback: () => void) => void Use cases:
  • Custom scheduling strategies
  • Testing (synchronous effects)
  • Server-side rendering
  • Performance optimization
Source: src/index.d.ts:335 Example - Synchronous effects for testing:
// In tests
options.requestAnimationFrame = (callback) => {
  callback(); // Execute immediately instead of async
};

debounceRendering

Control when state updates trigger re-renders:
options.debounceRendering = (callback) => {
  // Batch renders
  setTimeout(callback, 0);
};
Type: (callback: () => void) => void Use cases:
  • Batch multiple state updates
  • Control render timing
  • Testing (synchronous renders)
  • Performance optimization
Source: src/index.d.ts:336 Example - Synchronous rendering for tests:
// Store original
const originalDebounce = options.debounceRendering;

// In tests, render synchronously
options.debounceRendering = (callback) => callback();

// Restore after tests
afterEach(() => {
  options.debounceRendering = originalDebounce;
});

useDebugValue

Custom display for hook values in DevTools:
options.useDebugValue = (value) => {
  console.log('Hook value:', value);
};
Type: (value: string | number) => void Use cases:
  • Custom DevTools integration
  • Debugging custom hooks
  • Development logging
Source: src/index.d.ts:337

_addHookName

Add custom labels for hooks in DevTools:
options._addHookName = (name) => {
  // Called by addHookName() from preact/devtools
  console.log('Hook name:', name);
};
Type: (name: string | number) => void Source: src/index.d.ts:338, devtools/src/index.js:11-12

__suspenseDidResolve

Callback when Suspense boundary resolves:
options.__suspenseDidResolve = (vnode, callback) => {
  console.log('Suspense resolved:', vnode);
  callback();
};
Type: (vnode: VNode, callback: () => void) => void Use cases:
  • Track Suspense states
  • Custom loading behavior
  • Analytics
Source: src/index.d.ts:339

Internal Options Hooks

These hooks are used internally and by official addons. They’re not officially public but are stable:
Internal hooks may change between minor versions. Use with caution and test thoroughly when upgrading Preact.

_root

Invoked before rendering begins:
options._root = (vnode, parentNode) => {
  // Called by render() before starting
  console.log('Render starting');
};
Type: (vnode: VNode, parentNode: Element) => void Example from preact/debug:
options._root = (vnode, parentNode) => {
  if (!parentNode) {
    throw new Error(
      'Undefined parent passed to render(), this is the second argument.'
    );
  }
  
  // Validate parent is a valid DOM node
  if (parentNode.nodeType !== 1 && 
      parentNode.nodeType !== 11 && 
      parentNode.nodeType !== 9) {
    throw new Error('Expected a valid HTML node as second argument');
  }
};
Source: src/internal.d.ts:29, debug/src/debug.js:119-146

_diff

Invoked before a vnode is diffed:
options._diff = (vnode) => {
  // Validate vnode before diffing
  if (vnode.type === undefined) {
    throw new Error('Undefined component passed to createElement()');
  }
};
Type: (vnode: VNode) => void Example from preact/debug:
options._diff = (vnode) => {
  // Validate component type
  if (vnode.type === undefined) {
    throw new Error(
      'Undefined component passed to createElement()\n\n' +
      'You likely forgot to export your component or might have ' +
      'mixed up default and named imports'
    );
  }
  
  // Validate ref
  if (vnode.ref !== undefined && 
      typeof vnode.ref !== 'function' && 
      typeof vnode.ref !== 'object') {
    throw new Error(
      'Component\'s "ref" property should be a function or object'
    );
  }
};
Source: src/internal.d.ts:31, debug/src/debug.js:148-245

_render

Invoked before a component renders:
options._render = (vnode) => {
  // Called right before component function/render executes
  console.log('Rendering:', vnode.type);
};
Type: (vnode: VNode) => void Example - Infinite loop detection:
let renderCount = 0;
let currentComponent;

options._render = (vnode) => {
  const nextComponent = vnode._component;
  if (nextComponent === currentComponent) {
    renderCount++;
  } else {
    renderCount = 1;
  }
  
  if (renderCount >= 25) {
    throw new Error('Too many re-renders. Infinite loop detected!');
  }
  
  currentComponent = nextComponent;
};
Source: src/internal.d.ts:35, debug/src/debug.js:249-272

_commit

Invoked after DOM updates are committed:
options._commit = (vnode, commitQueue) => {
  // Called after DOM is updated
  commitQueue.forEach(component => {
    console.log('Committed:', component);
  });
};
Type: (vnode: VNode, commitQueue: Component[]) => void Use cases:
  • Track DOM mutations
  • Trigger side effects after paint
  • Integration with other libraries
Source: src/internal.d.ts:33

_hook

Invoked when hooks are called:
import { HookType } from 'preact';

options._hook = (component, index, type) => {
  if (type === HookType.useState) {
    console.log('useState called at index', index);
  }
};
Type: (component: Component, index: number, type: HookType) => void Hook types:
  • HookType.useState = 1
  • HookType.useReducer = 2
  • HookType.useEffect = 3
  • HookType.useLayoutEffect = 4
  • HookType.useRef = 5
  • HookType.useImperativeHandle = 6
  • HookType.useMemo = 7
  • HookType.useCallback = 8
  • HookType.useContext = 9
  • HookType.useErrorBoundary = 10
  • HookType.useDebugValue = 11
Source: src/internal.d.ts:3-16, src/internal.d.ts:37 Example - Hook validation:
let hooksAllowed = false;

options._render = () => {
  hooksAllowed = true;
};

options.diffed = () => {
  hooksAllowed = false;
};

options._hook = (component, index, type) => {
  if (!hooksAllowed) {
    throw new Error('Hook can only be invoked from render methods.');
  }
};
Source: debug/src/debug.js:59, debug/src/debug.js:274-280

_catchError

Called when an error is caught:
options._catchError = (error, vnode, oldVNode, errorInfo) => {
  console.error('Error caught:', error);
  console.log('Component stack:', errorInfo.componentStack);
};
Type: (error: any, vnode: VNode, oldVNode?: VNode, errorInfo?: ErrorInfo) => void Example from preact/debug:
options._catchError = (error, vnode, oldVNode, errorInfo) => {
  // Add component stack to error info
  errorInfo = errorInfo || {};
  errorInfo.componentStack = getOwnerStack(vnode);
  
  // Check for missing Suspense boundary
  if (typeof error.then === 'function') {
    let parent = vnode;
    let hasSuspense = false;
    
    for (; parent; parent = parent._parent) {
      if (parent._component && parent._component._childDidSuspend) {
        hasSuspense = true;
        break;
      }
    }
    
    if (!hasSuspense) {
      throw new Error(
        `Missing Suspense. The throwing component was: ${getDisplayName(vnode)}`
      );
    }
  }
};
Source: src/internal.d.ts:41-46, debug/src/debug.js:78-117

_hydrationMismatch

Called when hydration finds mismatched nodes:
options._hydrationMismatch = (vnode, excessDomChildren) => {
  const expected = vnode.type;
  const found = excessDomChildren
    .map(child => child && child.localName)
    .filter(Boolean);
  
  console.error(
    `Expected "${expected}" but found "${found.join(', ')}" during hydration`
  );
};
Type: (vnode: VNode, excessDomChildren: Element[]) => void Source: src/internal.d.ts:48-51, debug/src/debug.js:582-590

Chaining Options

When multiple libraries use the options API, they should chain previous hooks:
import { options } from 'preact';

// Save previous hook
const oldVNode = options.vnode;

// Install new hook that calls previous
options.vnode = (vnode) => {
  // Your logic
  console.log('My hook:', vnode.type);
  
  // Call previous hook
  if (oldVNode) oldVNode(vnode);
};
Example from preact/debug:
export function initDebug() {
  // Save all previous hooks
  let oldBeforeDiff = options._diff;
  let oldDiffed = options.diffed;
  let oldVnode = options.vnode;
  let oldRender = options._render;
  let oldCatchError = options._catchError;
  let oldRoot = options._root;
  let oldHook = options._hook;
  
  // Install new hooks that call previous ones
  options._diff = (vnode) => {
    // Debug logic here
    validateVNode(vnode);
    
    // Call previous hook
    if (oldBeforeDiff) oldBeforeDiff(vnode);
  };
  
  options.diffed = (vnode) => {
    // Debug logic here
    validateChildren(vnode);
    
    // Call previous hook
    if (oldDiffed) oldDiffed(vnode);
  };
  
  // ... more hooks
}
Source: debug/src/debug.js:56-68
Always chain previous hooks to avoid breaking other libraries that use the options API.

Complete Example: Custom Devtools

Here’s a complete example building a simple custom devtools:
import { options } from 'preact';

class SimpleDevtools {
  constructor() {
    this.componentTree = [];
    this.renderTimes = new Map();
    this.init();
  }
  
  init() {
    // Chain existing hooks
    const oldVNode = options.vnode;
    const oldDiffed = options.diffed;
    const oldUnmount = options.unmount;
    
    // Track component creation
    options.vnode = (vnode) => {
      if (typeof vnode.type === 'function') {
        vnode.startTime = performance.now();
        this.componentTree.push({
          name: vnode.type.displayName || vnode.type.name,
          props: { ...vnode.props },
          created: Date.now()
        });
      }
      if (oldVNode) oldVNode(vnode);
    };
    
    // Track render completion
    options.diffed = (vnode) => {
      if (vnode.startTime) {
        const duration = performance.now() - vnode.startTime;
        const name = vnode.type.displayName || vnode.type.name;
        
        if (!this.renderTimes.has(name)) {
          this.renderTimes.set(name, []);
        }
        this.renderTimes.get(name).push(duration);
      }
      if (oldDiffed) oldDiffed(vnode);
    };
    
    // Track unmounts
    options.unmount = (vnode) => {
      if (typeof vnode.type === 'function') {
        console.log('Unmounted:', vnode.type.name);
      }
      if (oldUnmount) oldUnmount(vnode);
    };
  }
  
  getStats() {
    const stats = {};
    for (const [name, times] of this.renderTimes) {
      const avg = times.reduce((a, b) => a + b, 0) / times.length;
      stats[name] = {
        renders: times.length,
        avgTime: avg.toFixed(2),
        totalTime: times.reduce((a, b) => a + b, 0).toFixed(2)
      };
    }
    return stats;
  }
}

// Usage
const devtools = new SimpleDevtools();

// Later, get stats
console.table(devtools.getStats());

Use Cases

The Options API enables:
  • Debug tools: Validation and error reporting (preact/debug)
  • DevTools integration: Component inspection (preact/devtools)
  • React compatibility: React API emulation (preact/compat)
  • Performance monitoring: Render tracking and profiling
  • Analytics: User interaction tracking
  • Testing utilities: Synchronous rendering (preact/test-utils)
  • Custom renderers: Alternative render targets
  • State management: External state integration

Best Practices

  1. Always chain hooks: Call previous hooks to support multiple libraries
  2. Minimize overhead: Keep hook logic lightweight
  3. Clean up: Remove hooks when done (restore previous values)
  4. TypeScript: Use proper types from preact/src/internal.d.ts
  5. Test thoroughly: Options hooks affect all components
  6. Document: Make it clear your library uses options hooks
  7. Dev only: Consider only enabling in development mode

TypeScript Support

Extend the Options interface for custom hooks:
import 'preact';

declare module 'preact' {
  export interface Options {
    // Add your custom hook
    _customHook?: (vnode: VNode) => void;
  }
}

// Now TypeScript recognizes your custom hook
options._customHook = (vnode) => {
  console.log('Custom hook called');
};

Performance Considerations

Options hooks are called frequently:
  • vnode: Every element/component creation
  • _diff: Before every diff
  • diffed: After every render
  • _render: Before every component render
Keep logic minimal to avoid performance impact:
// Bad: Expensive operation on every vnode
options.vnode = (vnode) => {
  JSON.stringify(vnode); // Slow!
  deepClone(vnode.props); // Slow!
};

// Good: Fast checks
options.vnode = (vnode) => {
  if (vnode.type === MyComponent) {
    // Only process specific components
  }
};

Build docs developers (and LLMs) love