Search code examples
javascriptreactjstypescriptreact-hooksrecaptcha

Getting this error "Invalid hook call. Hooks can only be called inside of the body of a function component." inside a react functional component?


I am receiving this error "Invalid hook call. Hooks can only be called inside of the body of a function component." when I try and run my function. I am trying to run a script that generates a reCaptcha token the script is as follows:

This component should generate the token(this is where I get the error)

import * as React from 'react'
import { RECAPTCHA_KEY } from '../../../utils/env'

// @ts-ignore
declare const window: any

interface RecaptchaProps {
  actionType: string
}
export const RecaptchaGen = ({ actionType }: RecaptchaProps) => {
  const siteKey = RECAPTCHA_KEY
  const [loaded, setLoaded] = React.useState(false)
  React.useEffect(() => {
    const scriptTag = document.createElement('script')
    scriptTag.src = 'https://www.google.com/recaptcha/api.js?render=' + siteKey
    document.appendChild(scriptTag)

    return () => {
      scriptTag.remove()
      setLoaded(false)
    }
  }, [siteKey])

  React.useEffect(() => {
    if (!loaded) return
    window.grecaptcha.ready(function () {
      window.grecaptcha
        .execute(siteKey, {
          action: actionType,
        })
        .then(function (token: string) {
          //reCaptch token generated needs to be sent to the back end for validation
          console.log(token)
        })
    })
  }, [loaded, actionType, siteKey])
}

Here within another component, I call the prior script component with an action type passed through but I get the react hook error mentioned(only showing relevant code snipets).

import * as React from 'react'
import { RecaptchaGen } from '../../../components/_molecules/Recaptcha'

const onLoginClick = () => {
    RecaptchaGen({ actionType: 'login' })
  }

I presume the issue is the way I am calling/using the imported component but I can't find a solution?


Solution

  • I am not familiar with your specific use case, but a common pattern is to encapsulate some of the effect logic into a function that is returned by your custom hook, then any normal JS function (callback or not) can invoke that function.

    1. Rename RecaptchaGen to useRecaptchaGen so it can be used as a React hook.

    2. Return a function to basically replace the second useEffect callback.

      export const useRecaptchaGen = ({ actionType }: RecaptchaProps) => {
        const siteKey = RECAPTCHA_KEY
        const [loaded, setLoaded] = React.useState(false)
        React.useEffect(() => {
          const scriptTag = document.createElement('script')
          scriptTag.src = 'https://www.google.com/recaptcha/api.js?render=' + siteKey
          document.appendChild(scriptTag)
      
          return () => {
            scriptTag.remove()
            setLoaded(false)
          }
        }, [siteKey]);
      
        const generateRecaptcha = () => {
          if (!loaded) return;
          window.grecaptcha.ready(function () {
            window.grecaptcha
              .execute(siteKey, {
                action: actionType,
              })
              .then(function (token: string) {
                //reCaptch token generated needs to be sent to the back end for validation
                console.log(token)
              })
          })
        };
      
        return { generateRecaptcha };
      }
      
    3. In the component use the new hook and destructure generateRecaptcha.

      const { generateRecaptcha } = useRecaptchaGen({ actionType: 'login' });
      
    4. Invoke generateRecaptcha in the onLoginClick handler.

      const onLoginClick = () => {
        generateRecaptcha();
      }