Skip to content

[bug] React: cap-widget throws null is not an object (evaluating 'this.#s._ensureSize') when unmounting #302

Description

@seratonik

bug description:
When cap-widget is used within a react component, and later gets unmounted from the page on navigate or otherwise, the following error can happen (but seems to have a race condition preventing it from always triggering, but it is frequent enough that a manual test case shows it, particularly on a fresh page load with caches cleared):

Unhandled Promise Rejection: TypeError: null is not an object (evaluating 'this.#s._ensureSize')

affects:

  • widget (ui) ("cap-widget": "0.1.56")

to reproduce:

  1. Create a react component to hold cap-widget like this:
import 'cap-widget';
import { useEffect, useRef } from 'react';

export default function CaptchaInput({
  api,
  onChange,
}: {
  api: any;
  onChange: (token: string) => void;
}) {
  if (!api) return console.error('CaptchaInput requires an API instance', api);

  const widgetRef = useRef<any | null>(null);

  const handleSolve = (e: CustomEvent) => {
    onChange(e.detail.token);
  };

  const handleError = (e: CustomEvent) => {
    console.error('Captcha error:', e.detail.message);
  };

  const handleProgress = (e: CustomEvent) => {
    console.log('Captcha progress:', e.detail.progress);
  };

  const handleReset = () => {
    console.log('Captcha reset');
    onChange('');
  };

  useEffect(() => {
    widgetRef.current?.addEventListener('solve', handleSolve);
    widgetRef.current?.addEventListener('error', handleError);
    widgetRef.current?.addEventListener('progress', handleProgress);
    widgetRef.current?.addEventListener('reset', handleReset);
    return () => {
      widgetRef.current?.removeEventListener('solve', handleSolve);
      widgetRef.current?.removeEventListener('error', handleError);
      widgetRef.current?.removeEventListener('progress', handleProgress);
      widgetRef.current?.removeEventListener('reset', handleReset);
    };
  }, [handleReset, handleProgress, handleError, handleSolve]);

  return <cap-widget ref={widgetRef} data-cap-api-endpoint={`${api.host()}/captcha`} />;
}
  1. Put that component on a page with a toggle button to conditionally render it
  2. Toggle it a bunch and observe errors in console

expected behavior:
Should unmount cleanly without errors

screenshots:
Not applicable

versions and environment:
Observed in:

  • Safari 26.5.2 on MacOS
  • Microsoft Edge 151 on Windows 11,
  • Chrome 151 on MacOS and Windows 11,
  • Mobile Safari 26.5.2 on iOS

additional context:
Doesn't seem to hold up subsequent Javascript execution, but creates a lot of noise when tracking errors via Sentry.

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions