Why use hCaptcha?

Captchas are an essential tool used to verify the authenticity of website users, distinguishing humans from automated bots. This helps to protect against malicious activities such as spamming, data scraping, and brute force attacks, thus enhancing online security and user experience.

While Google’s reCAPTCHA remains a popular choice, it is not without its drawbacks. As an alternative, hCaptcha provides a host of benefits that set it apart from reCAPTCHA. Unlike reCAPTCHA, hCaptcha minimises data sharing and storage, prioritising the safeguarding of users’ sensitive information. hCaptcha also offers a higher degree of customisation, allowing website owners to tailor the captcha difficulty level to suit their specific needs and user base. Additionally, hCaptcha offers the advantage of working in regions where Google services may be restricted or blocked

hCaptcha logo

How Does hCaptcha Work?

hCaptcha challenges are designed to be straightforward for humans to complete but more difficult for automated bots to solve. As the user interacts with the challenge, hCaptcha’s algorithms will analyse their behavior and responses, assessing various factors such as mouse moments, response times, accuracy and browser data.

hCaptcha Bots
hCaptcha is used to distinguish human users from automated bots. Image credit: hCaptcha

The Goal

Given the benefits of using hCaptcha, the goal was to replace all instances of reCAPTCHA with hCaptcha within an existing project. The project itself was built around a headless WordPress CMS with a Next.js frontend. The WordPress GravityForms plugin was used to manage server-side form validation. Within Next.js, a custom GravityForm component was responsible for rendering each form. On form submission, data was then sent to a POST api/form-submission endpoint for client-side validation.

Gravity Forms logo

Configuring hCaptcha on the Frontend

To integrate hCaptcha into the frontend, the react-hcaptcha component library was installed, replacing the existing reCAPTCHA equivalent. This package enabled the import of a HCaptcha child component into the GravityForm component. To connect to an existing hCaptcha account, a sitekey is defined as an environment variable and then passed into this component.

The react-hcaptcha component library

A value of “invisible” is passed to the HCaptcha component’s size prop to hide the checkbox widget on render. This means that users will only be presented with a challenge if they meet certain criteria. Additionally, the CSS property ‘display: none’ is applied to the widget container to prevent any additional whitespace on render.

// The HCaptcha component in action
<HCaptcha
  ref={hcaptchaRef}
  onVerify={submitForm}
  size="invisible"
/>

To initiate an hCaptcha challenge, the form’s handleSubmit function is called on form submission. By utilising the useRef hook, a reference to the HCaptcha component in the DOM is defined. Once the presence of this component is confirmed, a callback function is triggered which resets any previous challenge and programmatically triggers a new one using the execute instance method.

const handleSubmit = (event) => {
  event.preventDefault()

  // Programmatically invoking the captcha challenge
  if (hcaptchaRef && hcaptchaRef.current) {
    hcaptchaRef.current.resetCaptcha()
    hcaptchaRef.current.execute()
  }
}

If the user’s response is incorrect or the hCaptcha service is unsure about the validity of the response, the user may be re-prompted with a new challenge. Once the challenge is successfully completed, the onVerify prop is triggered. This callback handles the form submission process and appends the provided response token (h-captcha-response) to the form data.

// Submitting the h-captcha-response token with form data
const formData = new FormData(formRef.current)

formData.append('h-captcha-response', hcaptchaRef.current.getResponse())

const response = await fetch('/api/form-submission/', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(Object.fromEntries(formData)),
})

Configuring hCaptcha on the Backend

To integrate hCaptcha into the backend, the official hCaptcha for WordPress plugin was installed. Fortunately, this plugin supports GravityForms out of the box, resulting in minimal server-side changes. To connect to an existing hCaptcha account, the Site Key and Secret Key variables are included in the plugin settings.

Screenshot of the hCaptcha for WordPress plugin settings page
Adding the Site Key and Secret Key variables to the hCaptcha for WordPress plugin

Optimising Performance with Lazy Loading

Currently whenever we load a page with the component a decent amount of third party scripts are loaded in; in most cases a user may not even interact with any forms on the page yet they’re still having to load all of this javascript. This can be a large contributing factor to a poor performing PageSpeed Insights score. To combat this we can utilise Lazy Loading within Next.js.

Here’s a rundown on how we’ve composed our dynamic HCaptcha component.

Firstly we’ve created a “wrapper” component for the underlying @hcaptcha/react-hcaptcha component so that we can set some defaults (such as the site key) and provide a component to be dynamically imported.

import HCaptcha from '@hcaptcha/react-hcaptcha'

export default function WrappedHCaptcha({ hCaptchaRef, ...props }) {
  return (
    <HCaptcha
      ref={hCaptchaRef}
      sitekey={process.env.NEXT_PUBLIC_WORDPRESS_HCAPTCHA_SITE_KEY}
      {...props}
    />
  )
}

Secondly we’ve created two additional components to bring in the dynamic functionality. We have a DynamicHCaptcha component which utilises the next/dynamic API to pull in our WrappedHCaptcha component defined above.

Finally we define a fowardRef component HCaptcha which is what will be utilised within the application; by utilising the forwardRef we are able to forward all interactions with the defined ref down to the @hcaptcha/react-hcaptcha component.

import { forwardRef } from 'react'
import dynamic from 'next/dynamic'

// Component responsible for dynamically loading HCaptcha
const DynamicHCaptcha = dynamic(() => import('./WrappedHCaptcha'), {
  ssr: false,
})

// Component responsible for forwarding a ref that
// parent components can utilise to interact
// with the underlying HCatpcha component
const HCaptcha = forwardRef((props, ref) => (
  <DynamicHCaptcha {...props} hCaptchaRef={ref} />
))

export default HCaptcha

Here’s a simplified example of how it’s being utilised within a form. We’re controlling the inclusion of the HCaptcha component through the use of a showHCaptcha state that defaults to false and thus nothing is rendered when the page loads. It’s only when a user starts to fill out the form that we set this state to true, where the component is then dynamically loaded in.

import { useRef, useState } from 'react'

export default function Form() {
  const hcaptchaRef = useRef(null)
  const [showHCaptcha, setShowHCaptcha] = useState(false)

  const handleFormChange = () => {
    setShowHCaptcha(true)
  }

  const handleSubmit = (event) => {
    event.preventDefault()

    if (hcaptchaRef && hcaptchaRef.current) {
      hcaptchaRef.current.resetCaptcha()
      hcaptchaRef.current.execute()
    }
  }

  const submitForm = async () => {
    const formData = new FormData(formRef.current)

    formData.append('h-captcha-response', hcaptchaRef.current.getResponse())

    const response = await fetch('/api/form-submission/', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(Object.fromEntries(formData)),
    })
  }

  return (
    <form ref={formRef} onChange={handleFormChange} onSubmit={handleSubmit}>
      {/* fields */}

      {showHCaptcha && (
        <HCaptcha
          ref={hcaptchaRef}
          onVerify={submitForm}
          size="invisible"
        />
      )}

      <button>Submit</button>
    </form>
  )
}