Closed Antonio112009 closed 1 year ago
Yeah, I see a very weird behavior in the CodeSandbox:
https://codesandbox.io/p/sandbox/next-js-13-app-broken-b3qdyi
On the initial render context from the ReCaptchaProvider
loads fine, but after hydration it fallbacks to default values (nulls):
It's relatively easy to workaround on your side, by wrapping ReCaptchaProvider
into a separate client component:
https://codesandbox.io/p/sandbox/next-js-13-app-fixed-ujphw7
"use client";
import React from "react";
import { ReCaptchaProvider } from "next-recaptcha-v3";
const Providers = ({ children }) => (
<ReCaptchaProvider reCaptchaKey="6LfD67gkAAAAAP38z-nzxQ9YjhJsZAiIO0dpTVNi">
{children}
</ReCaptchaProvider>
);
export default Providers;
Looks like the client loses the context when doing hydration. Not sure why it happens, though. Our build file is marked as "use client", so it should work fine both on the server and client.
Will probably need some help with this ๐คจ
Will probably need to wait for this thing to resolve first: https://github.com/rollup/rollup/issues/4699
@snelsi Thank you so much for a quick fix! Really appreciate your work.
@snelsi It seems that the rollup issue has already been resolved.
https://github.com/rollup/rollup/issues/4699 leads to merged PR: https://github.com/inokawa/virtua/pull/103
Can you try to update this lib?
@prokopsimek
I've changed the rollup config to preserveModules: true
+ added preserveDirectives
plugin.
You can test this in 1.2.0-canary.0 version by running npm i next-recaptcha-v3@1.2.0-canary.0
@snelsi It didn't help. :(
The ReCaptcha works when I wrap the
in my layout.tsx with@prokopsimek
Can confirm that both cjs
and esm
output files include 'use client';
at the top ๐ค
Maybe, the Recaptcha has not been loaded
error not necessarily relates to RSC
I have no idea why this is happening, but again:
providers.tsx
"use client";
import { ReCaptchaProvider } from "next-recaptcha-v3";
const Providers = ({ children }) => <ReCaptchaProvider>{children}</ReCaptchaProvider>
export default Providers;
layout.tsx
import Providers from "./providers";
const Layout = ({ children }) => <Providers>{children}</Providers>
export default Layout;
layout.tsx
import { ReCaptchaProvider } from "next-recaptcha-v3";
const Layout = ({ children }) => <ReCaptchaProvider>{children}</ReCaptchaProvider>
export default Layout;
useReCaptcha
hook fallbacks to default placeholder value after hydration. It's like it doesn't see a context provider above it at all.
Could it be caused by server using cjs
and client using esm
...? ๐คจ
You're right.
components/Form.tsx
import { ReCaptchaProvider } from "next-recaptcha-v3";
const Form = ({ children }) => <ReCaptchaProvider>{children}</ReCaptchaProvider>
export default Form;
I'm not sure what's causing it. Any idea, @jdckl?
1.2.0-canary.3 version is now ESM ONLY
But it finally works in my localhost ๐
Still not for me.
package.json
"next-recaptcha-v3": "1.2.0-canary.3",
src/app/components/Providers.tsx
'use client';
import React from 'react';
import { ReCaptchaProvider } from 'next-recaptcha-v3';
const Providers = ({ children }: { children: React.ReactNode }) => (
<ReCaptchaProvider reCaptchaKey={process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY}>{children}</ReCaptchaProvider>
);
export default Providers;
src/app/components/ResourceForm.tsx
'use client';
import Link from 'next/link';
import Button from './Button';
import React, { useCallback, useState } from 'react';
import handleFormChange from './base/handleFormChange';
import { toast } from 'react-hot-toast';
import toastTemplate from './base/toastTemplate';
import axios from 'axios';
import { useReCaptcha } from 'next-recaptcha-v3';
import Providers from './Providers';
export default function ResourceForm(props: { mailchimpListId: string; tags: string }) {
const { mailchimpListId, tags } = props;
const [privacyAgreed, setPrivacyAgreed] = useState(false);
const [formData, setFormData] = useState({
email: '',
});
const { executeRecaptcha, loaded, error } = useReCaptcha();
const handleSubmit = useCallback(
async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!privacyAgreed) {
toast.custom((t) => toastTemplate(t, 'error', 'Uh oh!', 'You need to read and agree with the privacy policy!'), { id: 'privacy' });
return;
}
try {
//
} catch (e) {
//
}
},
[executeRecaptcha, formData, mailchimpListId, tags, privacyAgreed]
);
return (
<Providers>
<form className="mt-12" onSubmit={handleSubmit}>
<label htmlFor="email" className="sr-only mb-2 text-sm font-medium text-gray-900 dark:text-white">
Download
</label>
<div className="flex flex-col items-stretch md:flex-row">
<input
defaultValue={formData.email}
name="email"
onChange={(e) => handleFormChange(e, setFormData, formData)}
type="email"
id="email"
className="bg-white px-4 py-5 text-gray-900 md:grow"
placeholder="Your work email"
required
/>
<Button
className="mt-4 flex w-full items-center justify-center md:mt-0 md:w-auto md:justify-start"
label={'Download' + loaded + error}
formSubmit
style="primary"
/>
</div>
<div className="mt-6 flex flex-row gap-2">
<div className="flex h-6 items-center">
<input
id="remember"
type="checkbox"
value=""
onClick={() => setPrivacyAgreed(!privacyAgreed)}
defaultChecked={privacyAgreed}
className="h-6 w-6 border border-gray-300 bg-white accent-dxteal ring-dxteal checked:bg-dxteal hover:checked:bg-dxteal focus:outline-none focus:ring-dxteal focus:checked:bg-dxteal active:bg-dxteal"
/>
</div>
<label htmlFor="remember" className="ml-2 cursor-pointer select-none font-medium text-white">
I agree with the{' '}
<Link href="/privacy-policy.pdf" className="underline transition-all duration-300 ease-in-out hover:text-dxteal">
privacy policy
</Link>
.
</label>
</div>
</form>
</Providers>
);
}
Error: Recaptcha has not been loaded
at eval (webpack-internal:///(app-client)/./node_modules/next-recaptcha-v3/lib/useReCaptcha.js:27:19)
at eval (webpack-internal:///(app-client)/./src/app/components/ResourceForm.tsx:46:33)
at HTMLUnknownElement.callCallback (webpack-internal:///(app-client)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom.development.js:19449:14)
at Object.invokeGuardedCallbackImpl (webpack-internal:///(app-client)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom.development.js:19498:16)
at invokeGuardedCallback (webpack-internal:///(app-client)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom.development.js:19573:29)
at invokeGuardedCallbackAndCatchFirstError (webpack-internal:///(app-client)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom.development.js:19587:25)
at executeDispatch (webpack-internal:///(app-client)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom.development.js:30622:3)
at processDispatchQueueItemsInOrder (webpack-internal:///(app-client)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom.development.js:30654:7)
at processDispatchQueue (webpack-internal:///(app-client)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom.development.js:30667:5)
at dispatchEventsForPlugins (webpack-internal:///(app-client)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom.development.js:30678:3)
at eval (webpack-internal:///(app-client)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom.development.js:30868:12)
at batchedUpdates$1 (webpack-internal:///(app-client)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom.development.js:23765:12)
at batchedUpdates (webpack-internal:///(app-client)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom.development.js:27584:12)
at dispatchEventForPluginEventSystem (webpack-internal:///(app-client)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom.development.js:30867:3)
at dispatchEvent (webpack-internal:///(app-client)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom.development.js:28640:5)
at dispatchDiscreteEvent (webpack-internal:///(app-client)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom.development.js:28611:5)
@prokopsimek
1) You can safely remove this code:
reCaptchaKey={process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY}>
Provider will link it automatically.
2) You're trying to use useReCaptcha
outside of the ReCaptchaProvider
.
Context exists only for children inside of the provider. And in your case useReCaptcha
hook is located outside of it.
This template won't work:
const Component = () => {
const {} = useContext()
return (
<Context.Provider>
{children}
</Context.Provider>
)
}
Move your Provider up, like this:
<Context.Provider>
<ResourceForm />
</Context.Provider>
@snelsi I have moved it and the same error.
components/pages/ResourcesSection.tsx
'use client';
import Section from '../Section';
import Container from '../Container';
import { CMSClient } from '@/client/cmsClient';
import { LayoutResourcesComponent } from '@/client/sdk';
import Button from '../Button';
import { FiDownload } from 'react-icons/fi';
import Image from 'next/image';
import { BaseAnim } from '../base/baseAnim';
import Providers from '../Providers';
export default async function ResourcesSection(props: LayoutResourcesComponent) {
const resources = (await CMSClient.get.resources())?.data;
return (
<Providers>
<Section className="bg-light-700 py-16">
<Container>
<div className="grid grid-cols-1 gap-8 px-8 md:grid-cols-2 md:px-0">
{resources?.map((resource, i) => {
if (resource.attributes) {
const { title, slug, description, image } = resource.attributes;
return (
<BaseAnim key={i} className="flex flex-col items-center justify-center gap-10 border-2 border-white bg-white md:flex-row">
<div className="relative h-full overflow-hidden md:basis-1/3">
{image.data?.attributes?.url && (
<Image
className="min-h-full object-none"
alt={image.data?.attributes?.alternativeText || ''}
src={image.data?.attributes?.url}
width={image.data.attributes.width}
height={image.data.attributes.height}
/>
)}
</div>
<div className="flex flex-col gap-6 p-8 md:basis-2/3">
<h3>{title}</h3>
<p>{description}</p>
<Button icon={FiDownload} className="mt-6" href={`/resources/${slug}`} label="Download for free" style="secondary" />
</div>
</BaseAnim>
);
}
})}
</div>
</Container>
</Section>
</Providers>
);
}
components/ResourceForm.tsx
'use client';
import Link from 'next/link';
import Button from './Button';
import React, { useCallback, useState } from 'react';
import handleFormChange from './base/handleFormChange';
import { toast } from 'react-hot-toast';
import toastTemplate from './base/toastTemplate';
import axios from 'axios';
import { useReCaptcha } from 'next-recaptcha-v3';
export default function ResourceForm(props: { mailchimpListId: string; tags: string }) {
const { mailchimpListId, tags } = props;
const [privacyAgreed, setPrivacyAgreed] = useState(false);
const [formData, setFormData] = useState({
email: '',
});
const { executeRecaptcha, loaded, error } = useReCaptcha();
const handleSubmit = useCallback(
async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!privacyAgreed) {
toast.custom((t) => toastTemplate(t, 'error', 'Uh oh!', 'You need to read and agree with the privacy policy!'), { id: 'privacy' });
return;
}
try {
// Generate ReCaptcha token
const token = await executeRecaptcha('form_submit');
await axios.post('/api/form', { data: formData, listId: mailchimpListId, tags, token });
toast.custom((t) => toastTemplate(t, 'success', 'Good job!', 'You will receive the resource to your email momentarily!'));
} catch (e) {
console.error(e);
toast.custom((t) => toastTemplate(t, 'error', 'Something went bad!', 'Let us know, this should not be happening!'), {
id: 'error',
});
}
},
[executeRecaptcha, formData, mailchimpListId, tags, privacyAgreed]
);
return (
<form className="mt-12" onSubmit={handleSubmit}>
<label htmlFor="email" className="sr-only mb-2 text-sm font-medium text-gray-900 dark:text-white">
Download
</label>
<div className="flex flex-col items-stretch md:flex-row">
<input
defaultValue={formData.email}
name="email"
onChange={(e) => handleFormChange(e, setFormData, formData)}
type="email"
id="email"
className="bg-white px-4 py-5 text-gray-900 md:grow"
placeholder="Your work email"
required
/>
<Button
className="mt-4 flex w-full items-center justify-center md:mt-0 md:w-auto md:justify-start"
label={'Download' + loaded + error}
formSubmit
style="primary"
/>
</div>
<div className="mt-6 flex flex-row gap-2">
<div className="flex h-6 items-center">
<input
id="remember"
type="checkbox"
value=""
onClick={() => setPrivacyAgreed(!privacyAgreed)}
defaultChecked={privacyAgreed}
className="h-6 w-6 border border-gray-300 bg-white accent-dxteal ring-dxteal checked:bg-dxteal hover:checked:bg-dxteal focus:outline-none focus:ring-dxteal focus:checked:bg-dxteal active:bg-dxteal"
/>
</div>
<label htmlFor="remember" className="ml-2 cursor-pointer select-none font-medium text-white">
I agree with the{' '}
<Link href="/privacy-policy.pdf" className="underline transition-all duration-300 ease-in-out hover:text-dxteal">
privacy policy
</Link>
.
</label>
</div>
</form>
);
}
components/Providers.tsx
'use client';
import React from 'react';
import { ReCaptchaProvider } from 'next-recaptcha-v3';
const Providers = ({ children }: { children: React.ReactNode }) => <ReCaptchaProvider>{children}</ReCaptchaProvider>;
export default Providers;
Could it be because I am using it in a component and not in a page? ๐คท
As you can see, there is false/false
in the button. It's loaded/error
.
And the same error in console. :/
@prokopsimek
I don't see ResourceForm
component inside of your ResourcesSection
. Where do you render it?
@snelsi I am an idiot! ๐ It had to be inserted in resource/page.tsx
instead of my ResourceSection.tsx
.
It works. Thanks! ๐ฅ๐
This bug should hopefully be fixed in the 1.2.0 release ๐ค
Hi @snelsi,
Still seeing this error.
Versions: next@13.4.10 next-recaptcha-v3@1.2.0 react-hook-form@7.44.3
Using a custom component for RHF to load the grecaptcha_response to send to my api as follows:
import { useReCaptcha } from 'next-recaptcha-v3';
import { useCallback, useEffect, useState } from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import { type TInputProps } from './formInputProps';
const CaptchaInput = ({
control,
cta,
}: Pick<TInputProps, 'control'> & { cta: string }): JSX.Element => {
const [token, setToken] = useState('');
const { setValue } = useFormContext();
const { executeRecaptcha } = useReCaptcha();
const handleToken: (token: string) => void = useCallback(
(token: string): void => {
setToken(token);
setValue('grecaptcha_response', token);
setTimeout(() => {
const loadToken = async (): Promise<void> => {
const newToken = await executeRecaptcha(cta);
handleToken(newToken);
};
loadToken();
}, 120000);
},
[executeRecaptcha, setValue, cta],
);
useEffect(() => {
const loadToken = async (): Promise<void> => {
const newToken = await executeRecaptcha(cta);
handleToken(newToken);
};
loadToken();
}, [cta, executeRecaptcha, handleToken]);
return (
<Controller
name='grecaptcha_response'
control={control}
render={({ field: { onChange } }) => (
<input type='hidden' value={token} name='grecaptcha_response' onChange={onChange} />
)}
/>
);
};
export default CaptchaInput;
Have the ReCaptchaProvider
in my _app.tsx.
<ReCaptchaProvider reCaptchaKey={process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY ?? ''}>
<Component {...pageProps} />
</ReCaptchaProvider>
(And yes, the process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY has a value.)
The specific error comes from the executeRecaptcha
in the useEffect.
Any ideas?
@GoudekettingRM I feel like your code is a bit overenginired.
You can safely remove reCaptchaKey={process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY ?? ''}
.
ReCaptchaProvider
fallbacks to using process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY
by default.
Try checking if loaded
param is true
before calling executeRecaptcha
.
ReCaptcha
script takes some time to load, so you usually can't immediately generate a token on a page load.
I would suggest to update your code to look something like this:
import { useReCaptcha } from 'next-recaptcha-v3';
import { useEffect} from 'react';
import { useFormContext } from 'react-hook-form';
const CaptchaInput = ({
name = "grecaptcha_response",
cta = "form_submit",
}) => {
const form = useFormContext();
const { loaded, executeRecaptcha } = useReCaptcha();
useEffect(() => {
const loadToken = async () => {
const token = await executeRecaptcha(cta);
form.setValue(name, token);
};
if (loaded) loadToken();
}, [cta, loaded, executeRecaptcha]);
return <input {...form.register(name)} type='hidden' />;
};
export default CaptchaInput;
@snelsi Haha, I just wanted to reply that I read through the docs again and indeed am using the loaded
state from the hook now. That works like a charm. Thanks a lot!
@GoudekettingRM Glad you managed to make it work ๐
If you want, you can check out the code snippet I've posted above to simplify the logic inside of your component a bit.
If you have any additional questions, feel free to open a separate issue
@snelsi Yeah, I made it a bit simpler based of your snippet. Only thing we do seem to need is the refresh after 2 minutes, since google invalidates the tokens after that time. Have it like this now:
import { useReCaptcha } from 'next-recaptcha-v3';
import { useEffect } from 'react';
import { useFormContext } from 'react-hook-form';
const CaptchaInput = ({ cta }: { cta: string }): JSX.Element => {
const { setValue, register } = useFormContext();
const { executeRecaptcha, loaded } = useReCaptcha();
useEffect(() => {
let time: NodeJS.Timeout | null = null;
const loadToken = async (): Promise<void> => {
const token = await executeRecaptcha(cta);
setValue('grecaptcha_response', token);
time = setInterval(() => {
if (time) clearInterval(time);
loadToken();
}, 120000);
};
if (loaded) loadToken();
return () => {
if (time) clearInterval(time);
};
}, [cta, loaded, executeRecaptcha, setValue]);
return <input type='hidden' {...register('grecaptcha_response')} />;
};
export default CaptchaInput;
Hello! Im getting the same error using Nextjs with recaptcha v3 version. I still dont know what im doing wrong. I use the hook inside the provider. I can see the captcha being loaded on the website but it seems that the script is not loading since im getting the same error. I use the 1.4 version of next-recaptcha-v3.
Here is my code:
`export default function useCaptcha() { const { executeRecaptcha, loaded } = useReCaptcha()
const handleValidateCaptcha = useCallback(async () => {
try {
const captchaToken = await executeRecaptcha('click')
const response = await fetch(
'http://localhost:4321/api/recaptcha',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ token: captchaToken }),
},
)
return response
} catch (error) {
console.error('Captcha validation error:', error)
}
}, [executeRecaptcha, loaded])
const Providers = ({ children }: CaptchaProperties) => {
return (
<ReCaptchaProvider reCaptchaKey="">
{children}
</ReCaptchaProvider>
)
}
return {
Providers,
handleValidateCaptcha,
}
} `
Im using the environment variable, im just ommiting it for safety
I created a custom hook that returns the provider and a function that handles all the logic. Then I use a form inside the children on the provider.
All of the components are use client.
Describe the bug In the Nextjs 13.2, the token is "not loaded" when I request that the reCaptcha server generate a token. The server responds with the following message:
I am using v1.14 of the next-recaptcha-v3
To Reproduce Steps to reproduce the behavior:
app/layout.js
app/contact-us/page.js
Expected behavior A clear and concise description of what you expected to happen. The expected behaviour is that on submit, a token should be returned.
Screenshots If applicable, add screenshots to help explain your problem.
Desktop (please complete the following information):
Smartphone (please complete the following information):
Additional context Add any other context about the problem here.