preactjs/preact · error · Error
Component's "${key}" property should be a function, but got
Error message
Component's "${key}" property should be a function, but got [${typeof vnode.props[key]}] instead
${serializeVNode(vnode)}
${getOwnerStack(vnode)} What it means
Thrown from options._diff when the vnode type is a string (a host element like 'div') and a prop key starting with 'on' (onClick, onChange, etc.) is set to a non-null, non-function value. Preact treats on* props as event handlers; a non-function there would never fire and indicates a wiring bug. The message names the offending key and reports the actual typeof.
Source
Thrown at debug/src/debug.js:194
!('$$typeof' in vnode) // allow string refs when preact-compat is installed
) {
throw new Error(
`Component's "ref" property should be a function, or an object created ` +
`by createRef(), but got [${typeof vnode.ref}] instead\n` +
serializeVNode(vnode) +
`\n\n${getOwnerStack(vnode)}`
);
}
if (typeof vnode.type == 'string') {
for (const key in vnode.props) {
if (
key[0] === 'o' &&
key[1] === 'n' &&
typeof vnode.props[key] != 'function' &&
vnode.props[key] != null
) {
throw new Error(
`Component's "${key}" property should be a function, ` +
`but got [${typeof vnode.props[key]}] instead\n` +
serializeVNode(vnode) +
`\n\n${getOwnerStack(vnode)}`
);
}
}
}
// Check prop-types if available
if (typeof vnode.type == 'function' && vnode.type.propTypes) {
if (
vnode.type.displayName === 'Lazy' &&
warnedComponents &&
!warnedComponents.lazyPropTypes.has(vnode.type)
) {
const m =
'PropTypes are not supported on lazy(). Use propTypes on the wrapped component itself. ';View on GitHub (pinned to e881e7e838)
Solutions
- Replace the value with a function: `<button onClick={() => submit()}>` or `<button onClick={handleSubmit}>`.
- If the handler is optional, pass a no-op default: `onClick={onClose ?? (() => {})}` rather than a primitive.
- Check destructuring — ensure the variable bound to the on* prop is actually a function.
- For spread props, validate the source object's on* keys are functions before spreading.
Example fix
// before
<input onChange={value} />
// after
<input value={value} onChange={setValue} /> Defensive patterns
Strategy: type-guard
Validate before calling
function assertEventHandlers(props) {
for (const key in props) {
if (key.startsWith('on') && props[key] != null && typeof props[key] !== 'function') {
throw new Error(`Prop "${key}" must be a function, got ${typeof props[key]}`);
}
}
return props;
}
// before render
const safeProps = assertEventHandlers(inputProps);
return <input {...safeProps} />; Type guard
/** @param {string} key @param {unknown} v */
function isValidEventHandler(key, v) {
if (!key.startsWith('on')) return true;
return v == null || typeof v === 'function';
} Prevention
- Type on* props strictly: `onClick?: () => void` in TS prop interfaces.
- When spreading props, validate the source object's on* keys are functions.
- Never pass state values into on* slots — pass the setter or a handler.
When it happens
Trigger: Passing a string instead of a handler: `<button onClick='submit'>`; passing an object/number: `onChange={value}` where value is the state; destructuring `const { onClick } = props` then `<div onClick={onClick}/>` when onClick is undefined-but-not-null via a default; binding issues that yield a primitive; spread props where one on* key is a config value.
Common situations: Passing the value of an input into onChange instead of the handler: `onChange={text}` (state) vs `onChange={setText}`; mock or test prop objects where handlers are stubbed with strings; copy-paste of attribute name into handler slot; loose TS prop typing (`any`) hiding the mismatch until runtime.
Related errors
- Undefined parent passed to render(), this is the second argu
- Expected a valid HTML node as a second argument to render. R
- Undefined component passed to createElement() You likely fo
- Invalid type passed to createElement(): ${type} Did you acc
- Invalid type passed to createElement(): ${Array.isArray(type
AI-assisted analysis of preactjs/preact@e881e7e838 (2026-08-13).
Data as JSON: /api/errors/a551a7f8ea8f843d.
Report an issue: GitHub.