diff --git a/packages/react/src/components/presentation/auth/Recovery/BaseRecovery.tsx b/packages/react/src/components/presentation/auth/Recovery/BaseRecovery.tsx index 95c6d8d..b2ac86b 100644 --- a/packages/react/src/components/presentation/auth/Recovery/BaseRecovery.tsx +++ b/packages/react/src/components/presentation/auth/Recovery/BaseRecovery.tsx @@ -28,7 +28,7 @@ import { Preferences, FlowMetadataResponse, } from '@thunderid/browser'; -import {FC, ReactElement, ReactNode, useContext, useEffect, useState, useCallback, useRef} from 'react'; +import {FC, ReactElement, ReactNode, useContext, useEffect, useMemo, useState, useCallback, useRef} from 'react'; import ComponentRendererContext, { ComponentRendererMap, } from '../../../../contexts/ComponentRenderer/ComponentRendererContext'; @@ -226,7 +226,13 @@ const BaseRecoveryContent: FC = ({ [t], ); - const formFields: any = currentFlow?.data?.components ? extractFormFields(currentFlow.data.components) : []; + // Memoize the derived field list so its identity is stable across renders and + // does not cascade through `useForm` into an infinite update loop. See + // thunder-id/thunderid#3697. + const formFields: FormField[] = useMemo( + () => (currentFlow?.data?.components ? extractFormFields(currentFlow.data.components) : []), + [currentFlow, extractFormFields], + ); const form: any = useForm>({ fields: formFields, diff --git a/packages/react/src/components/presentation/auth/SignIn/BaseSignIn.tsx b/packages/react/src/components/presentation/auth/SignIn/BaseSignIn.tsx index 6d7ce50..9c230fc 100644 --- a/packages/react/src/components/presentation/auth/SignIn/BaseSignIn.tsx +++ b/packages/react/src/components/presentation/auth/SignIn/BaseSignIn.tsx @@ -25,7 +25,7 @@ import { FlowMetadataResponse, Preferences, } from '@thunderid/browser'; -import {FC, useState, useCallback, useContext, ReactElement, ReactNode} from 'react'; +import {FC, useEffect, useMemo, useState, useCallback, useContext, ReactElement, ReactNode} from 'react'; import useStyles from './BaseSignIn.styles'; import ComponentRendererContext, { ComponentRendererMap, @@ -230,11 +230,20 @@ export interface BaseSignInProps { variant?: CardProps['variant']; } +/** + * Stable empty-array reference used as the default for the optional `components` + * prop. A shared constant (rather than an inline `[]` default) keeps the reference + * identity stable across renders, so the memoized `formFields` below is not + * invalidated every render when a consumer omits `components`. + * See thunder-id/thunderid#3697. + */ +const EMPTY_COMPONENTS: EmbeddedFlowComponent[] = []; + /** * Internal component that consumes FlowContext and renders the sign-in UI. */ const BaseSignInContent: FC = ({ - components = [], + components = EMPTY_COMPONENTS, onSubmit, onError, error: externalError, @@ -332,7 +341,12 @@ const BaseSignInContent: FC = ({ [t], ); - const formFields: FormField[] = components ? extractFormFields(components) : []; + // Memoize the derived field list so its identity is stable across renders. + // Without this, a fresh array is created on every render, which cascades through + // `useForm` (getFieldConfig -> validateField -> setTouched), re-firing the + // server-error effect below on every render and causing an infinite update loop + // ("Maximum update depth exceeded"). See thunder-id/thunderid#3697. + const formFields: FormField[] = useMemo(() => extractFormFields(components), [components, extractFormFields]); const form: ReturnType = useForm>({ fields: formFields, diff --git a/packages/react/src/components/presentation/auth/SignUp/BaseSignUp.tsx b/packages/react/src/components/presentation/auth/SignUp/BaseSignUp.tsx index 4199750..bf76362 100644 --- a/packages/react/src/components/presentation/auth/SignUp/BaseSignUp.tsx +++ b/packages/react/src/components/presentation/auth/SignUp/BaseSignUp.tsx @@ -455,10 +455,13 @@ const BaseSignUpContent: FC = ({ [t], ); - const formFields: any = useMemo( - () => ((currentFlow?.data as any)?.components ? extractFormFields((currentFlow!.data as any).components) : []), - [currentFlow, extractFormFields], - ); + // Memoize the derived field list so its identity is stable across renders and + // does not cascade through `useForm` into an infinite update loop. See + // thunder-id/thunderid#3697. + const formFields: FormField[] = useMemo(() => { + const flowComponents: any = (currentFlow?.data as any)?.components; + return flowComponents ? extractFormFields(flowComponents) : []; + }, [currentFlow, extractFormFields]); const form: any = useForm>({ fields: formFields,