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

  1. Replace the value with a function: `<button onClick={() => submit()}>` or `<button onClick={handleSubmit}>`.
  2. If the handler is optional, pass a no-op default: `onClick={onClose ?? (() => {})}` rather than a primitive.
  3. Check destructuring — ensure the variable bound to the on* prop is actually a function.
  4. 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

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


AI-assisted analysis of preactjs/preact@e881e7e838 (2026-08-13). Data as JSON: /api/errors/a551a7f8ea8f843d. Report an issue: GitHub.