From 4acac09e0ba99c1a1fc0977e7969641b58bb89b2 Mon Sep 17 00:00:00 2001 From: webdevelopersrinu Date: Mon, 6 Jul 2026 11:16:45 +0530 Subject: [PATCH 1/3] Fix infinite render loop in auth flow components (#3697) --- .../presentation/auth/Recovery/BaseRecovery.tsx | 10 ++++++++-- .../presentation/auth/SignIn/BaseSignIn.tsx | 12 ++++++++++-- .../presentation/auth/SignUp/BaseSignUp.tsx | 13 +++++++++---- 3 files changed, 27 insertions(+), 8 deletions(-) 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 780238a..bec9080 100644 --- a/packages/react/src/components/presentation/auth/SignIn/BaseSignIn.tsx +++ b/packages/react/src/components/presentation/auth/SignIn/BaseSignIn.tsx @@ -26,7 +26,7 @@ import { Preferences, buildValidatorFromRules, } from '@thunderid/browser'; -import {FC, useEffect, 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, @@ -351,7 +351,15 @@ 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( + () => (components ? 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 96eb248..9f9f4f1 100644 --- a/packages/react/src/components/presentation/auth/SignUp/BaseSignUp.tsx +++ b/packages/react/src/components/presentation/auth/SignUp/BaseSignUp.tsx @@ -29,7 +29,7 @@ import { buildValidatorFromRules, Preferences, } from '@thunderid/browser'; -import {FC, ReactElement, ReactNode, useCallback, useContext, useEffect, useRef, useState} from 'react'; +import {FC, ReactElement, ReactNode, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; import useStyles from './BaseSignUp.styles'; import ComponentRendererContext, { ComponentRendererMap, @@ -455,9 +455,14 @@ const BaseSignUpContent: FC = ({ [t], ); - const formFields: any = (currentFlow?.data as any)?.components - ? extractFormFields((currentFlow!.data as any).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 as any)?.components ? extractFormFields((currentFlow!.data as any).components) : [], + [currentFlow, extractFormFields], + ); const form: any = useForm>({ fields: formFields, From 0fccaeb1fc01b417a2b386a2268908faba7777ac Mon Sep 17 00:00:00 2001 From: webdevelopersrinu Date: Mon, 6 Jul 2026 11:44:29 +0530 Subject: [PATCH 2/3] Address review: stable EMPTY_COMPONENTS default + drop redundant assertion (#3697) --- .../presentation/auth/SignIn/BaseSignIn.tsx | 11 ++++++++++- .../presentation/auth/SignUp/BaseSignUp.tsx | 9 ++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/react/src/components/presentation/auth/SignIn/BaseSignIn.tsx b/packages/react/src/components/presentation/auth/SignIn/BaseSignIn.tsx index bec9080..f1db50e 100644 --- a/packages/react/src/components/presentation/auth/SignIn/BaseSignIn.tsx +++ b/packages/react/src/components/presentation/auth/SignIn/BaseSignIn.tsx @@ -236,11 +236,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, diff --git a/packages/react/src/components/presentation/auth/SignUp/BaseSignUp.tsx b/packages/react/src/components/presentation/auth/SignUp/BaseSignUp.tsx index 9f9f4f1..bf76362 100644 --- a/packages/react/src/components/presentation/auth/SignUp/BaseSignUp.tsx +++ b/packages/react/src/components/presentation/auth/SignUp/BaseSignUp.tsx @@ -458,11 +458,10 @@ const BaseSignUpContent: FC = ({ // 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 as any)?.components ? extractFormFields((currentFlow!.data as any).components) : [], - [currentFlow, extractFormFields], - ); + const formFields: FormField[] = useMemo(() => { + const flowComponents: any = (currentFlow?.data as any)?.components; + return flowComponents ? extractFormFields(flowComponents) : []; + }, [currentFlow, extractFormFields]); const form: any = useForm>({ fields: formFields, From 72d97b1cecc232fe74f5dcbf332c237536beaee8 Mon Sep 17 00:00:00 2001 From: webdevelopersrinu Date: Tue, 7 Jul 2026 17:29:05 +0530 Subject: [PATCH 3/3] Simplify formFields memo now that components has a stable default (#3697) --- .../src/components/presentation/auth/SignIn/BaseSignIn.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/react/src/components/presentation/auth/SignIn/BaseSignIn.tsx b/packages/react/src/components/presentation/auth/SignIn/BaseSignIn.tsx index caeef3f..9c230fc 100644 --- a/packages/react/src/components/presentation/auth/SignIn/BaseSignIn.tsx +++ b/packages/react/src/components/presentation/auth/SignIn/BaseSignIn.tsx @@ -346,10 +346,7 @@ const BaseSignInContent: FC = ({ // `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( - () => (components ? extractFormFields(components) : []), - [components, extractFormFields], - ); + const formFields: FormField[] = useMemo(() => extractFormFields(components), [components, extractFormFields]); const form: ReturnType = useForm>({ fields: formFields,