Description
Operating System
Manjaro
Environment (if applicable)
node 23.3, React
Firebase SDK Version
"firebase": "^11.1.0",
Firebase SDK Product(s)
Auth
Project Tooling
It is using react 18.3 with vite 6.0.01.
Detailed Problem Description
I am trying to build ReCaptchaButton component in React JS. For this to work I need to be able to create a RecaptchaVerfier as soon as the elment is mounted and destroy it as soon as it is unmounted. Unfortunately, the destroy part doesn't work. (In Strict Mode, React mounts a component, then immedietly unmounts it and then mounts a new instance, to enforce re-usability)
This wouldn't be a problem, but unfortunately, recaptchaVerifier.clear doesn't seem to actually do anything. Meaning, it stays attached to the dom (which react keeps as is) even as the component itself unmounts. The next mount then encounters an already captcha infested button and then throws an Error.
The only way I've been able to sort of make this work, is by anticipating the remount using a window.alreadyMounted variable and then reuse the verfier of the already unmounted component. This is an ugly hack which itself spits out a bunch of errors, namely:
Uncaught TypeError: n is null
N recaptcha__en.js:514
F recaptcha__en.js:152
and that is despite my efforts to suppress thrown errors. From what I've found, nobody has yet been able to integrated firebase embedded RecaptchaVerfier into a clean React component, simply because .clear doesn't actually clear the component.
Steps and code to reproduce issue
Here's the RecaptchaButton. Simply Mounting it once will spit out the Error: reCAPTCHA has already been rendered in this element N recaptcha__en.js:477 Error
import {
RecaptchaVerifier
} from "firebase/auth";
import {useState,useContext,useEffect} from 'react';
import * as Bootstrap from 'react-bootstrap';
import {AuthSingleton} from '../auth/AuthSingleton.js';
import * as auth from 'firebase/auth';
//Need these because recaptch is a mess
function randomAlphaNumeric(length = 64) {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
const randomValues = new Uint8Array(length);
crypto.getRandomValues(randomValues);
for (let i = 0; i < length; i++) {
result += chars[randomValues[i] % chars.length];
}
return result;
}
export function ReCaptchaButton({onVerified,onError,...props}) {
const [randomizedKey,setRandomizedKey]=useState(randomAlphaNumeric(64));
useEffect(()=>{
const recaptchaVerifier = new RecaptchaVerifier(AuthSingleton.get().firebaseAuth, randomizedKey, {
"size": "invisible",
"callback": async function(response) {
// reCAPTCHA solved, you can proceed with
// phoneAuthProvider.verifyPhoneNumber(...).
onVerified(recaptchaVerifier);
},
"expired-callback": function() {
// Response expired. Ask user to solve reCAPTCHA again.
onError(new Error("Recaptcha expired"));
}
,
"error-callback": function(e) {
onError(e);
}
});
try{
recaptchaVerifier.render().catch(()=>{});
}catch(e){
}
// This will be executed as soon as the component unmounts
return ()=>{
// This doesn't work
recaptchaVerifier.clear();
};
},[]);
return (
<Bootstrap.Button {...props} id={randomizedKey} key={randomizedKey} />
);
}