Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -226,7 +226,13 @@ const BaseRecoveryContent: FC<BaseRecoveryProps> = ({
[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<Record<string, string>>({
fields: formFields,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<BaseSignInProps> = ({
components = [],
components = EMPTY_COMPONENTS,
onSubmit,
onError,
error: externalError,
Expand Down Expand Up @@ -332,7 +341,12 @@ const BaseSignInContent: FC<BaseSignInProps> = ({
[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<typeof useForm> = useForm<Record<string, string>>({
fields: formFields,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -455,10 +455,13 @@ const BaseSignUpContent: FC<BaseSignUpProps> = ({
[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<Record<string, string>>({
fields: formFields,
Expand Down