From b03ebb5ac14015e16710f0123904bfa76d85a630 Mon Sep 17 00:00:00 2001 From: William Lubelski Date: Wed, 25 Oct 2023 11:54:01 -0700 Subject: [PATCH 1/5] stabilize onVerify callback to prevent infinite loop --- src/google-recaptcha.tsx | 19 +++++++++++++++---- src/use-stable-callback.ts | 25 +++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 4 deletions(-) create mode 100644 src/use-stable-callback.ts diff --git a/src/google-recaptcha.tsx b/src/google-recaptcha.tsx index ca43eae..e21b68d 100644 --- a/src/google-recaptcha.tsx +++ b/src/google-recaptcha.tsx @@ -1,5 +1,6 @@ import React, { useEffect } from 'react'; import { useGoogleReCaptcha } from './use-google-recaptcha'; +import { useStableCallback } from './use-stable-callback'; import { logWarningMessage } from './utils'; export interface IGoogleRecaptchaProps { @@ -11,10 +12,14 @@ export interface IGoogleRecaptchaProps { export function GoogleReCaptcha({ action, onVerify, - refreshReCaptcha, + refreshReCaptcha }: IGoogleRecaptchaProps) { const googleRecaptchaContextValue = useGoogleReCaptcha(); + const hasVerify = !!onVerify; + + const handleVerify = useStableCallback(onVerify); + useEffect(() => { const { executeRecaptcha } = googleRecaptchaContextValue; @@ -25,17 +30,23 @@ export function GoogleReCaptcha({ const handleExecuteRecaptcha = async () => { const token = await executeRecaptcha(action); - if (!onVerify) { + if (!hasVerify) { logWarningMessage('Please define an onVerify function'); return; } - onVerify(token); + handleVerify(token); }; handleExecuteRecaptcha(); - }, [action, onVerify, refreshReCaptcha, googleRecaptchaContextValue]); + }, [ + action, + handleVerify, + hasVerify, + refreshReCaptcha, + googleRecaptchaContextValue + ]); const { container } = googleRecaptchaContextValue; diff --git a/src/use-stable-callback.ts b/src/use-stable-callback.ts new file mode 100644 index 0000000..22f0e47 --- /dev/null +++ b/src/use-stable-callback.ts @@ -0,0 +1,25 @@ +import { useRef, useEffect, useState } from 'react'; + +type AnyFunc = (...args: any[]) => any | undefined; + +const useStableCallback = (fn: T): T => { + const ref = useRef(fn); + + useEffect(() => { + ref.current = fn; + }, [fn]); + + const [stableFn] = useState(() => { + const newFn = (...args: any[]) => { + if (ref.current) { + return ref.current(...args); + } + }; + + return newFn as T; + }); + + return stableFn; +}; + +export { useStableCallback }; From e900f52bc070e35baa043207ce31062fbee7dbe4 Mon Sep 17 00:00:00 2001 From: William Lubelski Date: Wed, 25 Oct 2023 11:58:21 -0700 Subject: [PATCH 2/5] remove note from readme --- README.md | 53 ++++++++++++---------------------------- src/google-recaptcha.tsx | 6 +++++ 2 files changed, 21 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 7ac89db..aad4ca1 100644 --- a/README.md +++ b/README.md @@ -41,15 +41,15 @@ Usually, your application only needs one provider. You should place it as high a Same thing applied when you use this library with framework such as Next.js or React Router and only want to include the script on a single page. Try to make sure you only have one instance of the provider on a React tree and to place it as high (on the tree) as possible. -| **Props** | **Type** | **Default** | **Required?** | **Note** | -|----------------------|:----------------:| ----------: | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| reCaptchaKey | String | | Yes | Your recaptcha key, get one from [here](https://www.google.com/recaptcha/intro/v3.html) | -| scriptProps | Object | | No | You can customize the injected `script` tag with this prop. It allows you to add `async`, `defer`, `nonce` attributes to the script tag. You can also control whether the injected script will be added to the document body or head with `appendTo` attribute. | -| language | String | | No | optional prop to support different languages that is supported by Google Recaptcha. https://developers.google.com/recaptcha/docs/language | -| useRecaptchaNet | Boolean | false | No | The provider also provide the prop `useRecaptchaNet` to load script from `recaptcha.net`: https://developers.google.com/recaptcha/docs/faq#can-i-use-recaptcha-globally | -| useEnterprise | Boolean | false | No | [Enterprise option](#enterprise) | +| **Props** | **Type** | **Default** | **Required?** | **Note** | +| -------------------- | :----------------: | ----------: | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| reCaptchaKey | String | | Yes | Your recaptcha key, get one from [here](https://www.google.com/recaptcha/intro/v3.html) | +| scriptProps | Object | | No | You can customize the injected `script` tag with this prop. It allows you to add `async`, `defer`, `nonce` attributes to the script tag. You can also control whether the injected script will be added to the document body or head with `appendTo` attribute. | +| language | String | | No | optional prop to support different languages that is supported by Google Recaptcha. https://developers.google.com/recaptcha/docs/language | +| useRecaptchaNet | Boolean | false | No | The provider also provide the prop `useRecaptchaNet` to load script from `recaptcha.net`: https://developers.google.com/recaptcha/docs/faq#can-i-use-recaptcha-globally | +| useEnterprise | Boolean | false | No | [Enterprise option](#enterprise) | | container.element | String HTMLElement | | No | Container ID where the recaptcha badge will be rendered | -| container.parameters | Object | | No | Configuration for the inline badge (See google recaptcha docs) | +| container.parameters | Object | | No | Configuration for the inline badge (See google recaptcha docs) | ```javascript import { GoogleReCaptchaProvider } from 'react-google-recaptcha-v3'; @@ -66,11 +66,12 @@ ReactDom.render( appendTo: 'head', // optional, default to "head", can be "head" or "body", nonce: undefined // optional, default undefined }} - container={{ // optional to render inside custom element - element: "[required_id_or_htmlelement]", + container={{ + // optional to render inside custom element + element: '[required_id_or_htmlelement]', parameters: { badge: '[inline|bottomright|bottomleft]', // optional, default undefined - theme: 'dark', // optional, default undefined + theme: 'dark' // optional, default undefined } }} > @@ -100,28 +101,6 @@ ReactDom.render( ); ``` -```javascript -// IMPORTANT NOTES: The `GoogleReCaptcha` component is a wrapper around `useGoogleRecaptcha` hook and use `useEffect` to run the verification. -// It's important that you understand how React hooks work to use it properly. -// Avoid using inline function for the `onVerify` props as it can possibly cause the verify function to run continously. -// To avoid that problem, you can use a memoized function provided by `React.useCallback` or a class method -// The code below is an example that inline function can result in an infinite loop and the verify function runs continously: - -const MyComponent: FC = () => { - const [token, setToken] = useState(); - - return ( -
- { - setToken(token); - }} - /> -
- ); -}; -``` - ```javascript // Example of refreshReCaptcha option: @@ -129,14 +108,14 @@ const MyComponent: FC = () => { const [token, setToken] = useState(); const [refreshReCaptcha, setRefreshReCaptcha] = useState(false); - const onVerify = useCallback((token) => { + const onVerify = useCallback(token => { setToken(token); }); const doSomething = () => { /* do something like submit a form and then refresh recaptcha */ setRefreshReCaptcha(r => !r); - } + }; return (
@@ -144,9 +123,7 @@ const MyComponent: FC = () => { onVerify={onVerify} refreshReCaptcha={refreshReCaptcha} /> - +
); }; diff --git a/src/google-recaptcha.tsx b/src/google-recaptcha.tsx index e21b68d..030c44c 100644 --- a/src/google-recaptcha.tsx +++ b/src/google-recaptcha.tsx @@ -3,6 +3,12 @@ import { useGoogleReCaptcha } from './use-google-recaptcha'; import { useStableCallback } from './use-stable-callback'; import { logWarningMessage } from './utils'; +/* +to avoid onVerify tripping the use effect on every render, +the useStableCallback hook is used to expose a stable reference +that will stay up to date if a new version is passed in +*/ + export interface IGoogleRecaptchaProps { onVerify: (token: string) => void | Promise; action?: string; From e8f7c0894cea9400fe1162be4fa140715dbdfe6f Mon Sep 17 00:00:00 2001 From: William Lubelski Date: Wed, 25 Oct 2023 11:58:45 -0700 Subject: [PATCH 3/5] Revert "remove note from readme" This reverts commit e900f52bc070e35baa043207ce31062fbee7dbe4. --- README.md | 53 ++++++++++++++++++++++++++++------------ src/google-recaptcha.tsx | 6 ----- 2 files changed, 38 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index aad4ca1..7ac89db 100644 --- a/README.md +++ b/README.md @@ -41,15 +41,15 @@ Usually, your application only needs one provider. You should place it as high a Same thing applied when you use this library with framework such as Next.js or React Router and only want to include the script on a single page. Try to make sure you only have one instance of the provider on a React tree and to place it as high (on the tree) as possible. -| **Props** | **Type** | **Default** | **Required?** | **Note** | -| -------------------- | :----------------: | ----------: | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| reCaptchaKey | String | | Yes | Your recaptcha key, get one from [here](https://www.google.com/recaptcha/intro/v3.html) | -| scriptProps | Object | | No | You can customize the injected `script` tag with this prop. It allows you to add `async`, `defer`, `nonce` attributes to the script tag. You can also control whether the injected script will be added to the document body or head with `appendTo` attribute. | -| language | String | | No | optional prop to support different languages that is supported by Google Recaptcha. https://developers.google.com/recaptcha/docs/language | -| useRecaptchaNet | Boolean | false | No | The provider also provide the prop `useRecaptchaNet` to load script from `recaptcha.net`: https://developers.google.com/recaptcha/docs/faq#can-i-use-recaptcha-globally | -| useEnterprise | Boolean | false | No | [Enterprise option](#enterprise) | +| **Props** | **Type** | **Default** | **Required?** | **Note** | +|----------------------|:----------------:| ----------: | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| reCaptchaKey | String | | Yes | Your recaptcha key, get one from [here](https://www.google.com/recaptcha/intro/v3.html) | +| scriptProps | Object | | No | You can customize the injected `script` tag with this prop. It allows you to add `async`, `defer`, `nonce` attributes to the script tag. You can also control whether the injected script will be added to the document body or head with `appendTo` attribute. | +| language | String | | No | optional prop to support different languages that is supported by Google Recaptcha. https://developers.google.com/recaptcha/docs/language | +| useRecaptchaNet | Boolean | false | No | The provider also provide the prop `useRecaptchaNet` to load script from `recaptcha.net`: https://developers.google.com/recaptcha/docs/faq#can-i-use-recaptcha-globally | +| useEnterprise | Boolean | false | No | [Enterprise option](#enterprise) | | container.element | String HTMLElement | | No | Container ID where the recaptcha badge will be rendered | -| container.parameters | Object | | No | Configuration for the inline badge (See google recaptcha docs) | +| container.parameters | Object | | No | Configuration for the inline badge (See google recaptcha docs) | ```javascript import { GoogleReCaptchaProvider } from 'react-google-recaptcha-v3'; @@ -66,12 +66,11 @@ ReactDom.render( appendTo: 'head', // optional, default to "head", can be "head" or "body", nonce: undefined // optional, default undefined }} - container={{ - // optional to render inside custom element - element: '[required_id_or_htmlelement]', + container={{ // optional to render inside custom element + element: "[required_id_or_htmlelement]", parameters: { badge: '[inline|bottomright|bottomleft]', // optional, default undefined - theme: 'dark' // optional, default undefined + theme: 'dark', // optional, default undefined } }} > @@ -101,6 +100,28 @@ ReactDom.render( ); ``` +```javascript +// IMPORTANT NOTES: The `GoogleReCaptcha` component is a wrapper around `useGoogleRecaptcha` hook and use `useEffect` to run the verification. +// It's important that you understand how React hooks work to use it properly. +// Avoid using inline function for the `onVerify` props as it can possibly cause the verify function to run continously. +// To avoid that problem, you can use a memoized function provided by `React.useCallback` or a class method +// The code below is an example that inline function can result in an infinite loop and the verify function runs continously: + +const MyComponent: FC = () => { + const [token, setToken] = useState(); + + return ( +
+ { + setToken(token); + }} + /> +
+ ); +}; +``` + ```javascript // Example of refreshReCaptcha option: @@ -108,14 +129,14 @@ const MyComponent: FC = () => { const [token, setToken] = useState(); const [refreshReCaptcha, setRefreshReCaptcha] = useState(false); - const onVerify = useCallback(token => { + const onVerify = useCallback((token) => { setToken(token); }); const doSomething = () => { /* do something like submit a form and then refresh recaptcha */ setRefreshReCaptcha(r => !r); - }; + } return (
@@ -123,7 +144,9 @@ const MyComponent: FC = () => { onVerify={onVerify} refreshReCaptcha={refreshReCaptcha} /> - +
); }; diff --git a/src/google-recaptcha.tsx b/src/google-recaptcha.tsx index 030c44c..e21b68d 100644 --- a/src/google-recaptcha.tsx +++ b/src/google-recaptcha.tsx @@ -3,12 +3,6 @@ import { useGoogleReCaptcha } from './use-google-recaptcha'; import { useStableCallback } from './use-stable-callback'; import { logWarningMessage } from './utils'; -/* -to avoid onVerify tripping the use effect on every render, -the useStableCallback hook is used to expose a stable reference -that will stay up to date if a new version is passed in -*/ - export interface IGoogleRecaptchaProps { onVerify: (token: string) => void | Promise; action?: string; From d6acf9799a98b7f9329b9d31c9b08591500ac8a3 Mon Sep 17 00:00:00 2001 From: William Lubelski Date: Wed, 25 Oct 2023 12:02:58 -0700 Subject: [PATCH 4/5] remove note from readme --- README.md | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/README.md b/README.md index 7ac89db..c6b6e99 100644 --- a/README.md +++ b/README.md @@ -100,28 +100,6 @@ ReactDom.render( ); ``` -```javascript -// IMPORTANT NOTES: The `GoogleReCaptcha` component is a wrapper around `useGoogleRecaptcha` hook and use `useEffect` to run the verification. -// It's important that you understand how React hooks work to use it properly. -// Avoid using inline function for the `onVerify` props as it can possibly cause the verify function to run continously. -// To avoid that problem, you can use a memoized function provided by `React.useCallback` or a class method -// The code below is an example that inline function can result in an infinite loop and the verify function runs continously: - -const MyComponent: FC = () => { - const [token, setToken] = useState(); - - return ( -
- { - setToken(token); - }} - /> -
- ); -}; -``` - ```javascript // Example of refreshReCaptcha option: From 0fd3254dd7fc49aabaa95994eec8ef3660693ea4 Mon Sep 17 00:00:00 2001 From: William Lubelski Date: Wed, 25 Oct 2023 12:03:04 -0700 Subject: [PATCH 5/5] add comment to hook --- src/google-recaptcha.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/google-recaptcha.tsx b/src/google-recaptcha.tsx index e21b68d..4185cd0 100644 --- a/src/google-recaptcha.tsx +++ b/src/google-recaptcha.tsx @@ -18,6 +18,9 @@ export function GoogleReCaptcha({ const hasVerify = !!onVerify; + // handleVerify is a stable reference + // and therefore will not trip the useEffect into an infinite loop + // when onVerify is an anonymous or otherwise changing function. const handleVerify = useStableCallback(onVerify); useEffect(() => {