diff --git a/apps/myaccount/src/components/account-recovery/options/security-questions-recovery.tsx b/apps/myaccount/src/components/account-recovery/options/security-questions-recovery.tsx index a8b232a505a..d19ee30ee48 100644 --- a/apps/myaccount/src/components/account-recovery/options/security-questions-recovery.tsx +++ b/apps/myaccount/src/components/account-recovery/options/security-questions-recovery.tsx @@ -22,7 +22,7 @@ import { TestableComponentInterface, import { Field, FormValue, Forms } from "@wso2is/forms/legacy"; import { GenericIcon } from "@wso2is/react-components"; import { AxiosError } from "axios"; -import React, { ReactElement, useEffect, useState } from "react"; +import React, { MutableRefObject, ReactElement, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { useDispatch, useSelector } from "react-redux"; import { Dispatch } from "redux"; @@ -70,7 +70,8 @@ export const SecurityQuestionsComponent: React.FunctionComponent(); const [ challenges, setChallenges ] = useState(createEmptyChallenge()); const [ isEdit, setIsEdit ] = useState(-1); - const [ isInit, setIsInit ] = useState(false); + // Guards a one-time fetch on mount; only read/written in handlers, never rendered, so a ref avoids a re-render. + const isInitRef: MutableRefObject = useRef(false); const activeForm: string = useSelector((state: AppState) => state.global.activeForm); @@ -82,7 +83,7 @@ export const SecurityQuestionsComponent: React.FunctionComponent { - setIsInit(true); + isInitRef.current = true; setChallenges({ answers: [ ...response[1] ], isEdit: false, @@ -292,7 +293,7 @@ export const SecurityQuestionsComponent: React.FunctionComponent { - if (!isInit) { + if (!isInitRef.current) { getSecurityQs().then((response: any) => { setSecurityDetails(response); }); diff --git a/apps/myaccount/src/components/consents/consents.tsx b/apps/myaccount/src/components/consents/consents.tsx index 6b8dff1e1e4..ad9969d25d6 100644 --- a/apps/myaccount/src/components/consents/consents.tsx +++ b/apps/myaccount/src/components/consents/consents.tsx @@ -19,7 +19,7 @@ import { TestableComponentInterface } from "@wso2is/core/models"; import cloneDeep from "lodash-es/cloneDeep"; import flatten from "lodash-es/flatten"; -import React, { FunctionComponent, useEffect, useState } from "react"; +import React, { FunctionComponent, MutableRefObject, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { useSelector } from "react-redux"; import { Message, Modal } from "semantic-ui-react"; @@ -70,7 +70,8 @@ export const Consents: FunctionComponent = (props: Consen const { onAlertFired, ["data-testid"]: testId } = props; const [ consentedApps, setConsentedApps ] = useState([]); - const [ purposeDetailModels, setPurposeDetailModels ] = useState([]); + // Cache of purpose models, only read/written inside handlers, so a ref avoids unnecessary re-renders. + const purposeDetailModelsRef: MutableRefObject = useRef([]); const [ revokingConsent, setRevokingConsent ] = useState(); const [ isConsentRevokeModalVisible, setConsentRevokeModalVisibility ] = useState(false); const [ consentListActiveIndexes, setConsentListActiveIndexes ] = useState([]); @@ -152,7 +153,7 @@ export const Consents: FunctionComponent = (props: Consen const response: PurposeModel[] = await fetchPurposesByIDs(purposesIds); // Set response value to the hook - setPurposeDetailModels(response); + purposeDetailModelsRef.current = response; }; /** @@ -211,7 +212,7 @@ export const Consents: FunctionComponent = (props: Consen const attachResidentIDPReceiptMissingPurposes = async (receipt: ConsentReceiptInterface): Promise => { // Filter out the non-default purposes from the cached {@link purposeModels} - const allPurposeModelsExceptDefault: PurposeModel[] = purposeDetailModels.filter( + const allPurposeModelsExceptDefault: PurposeModel[] = purposeDetailModelsRef.current.filter( ({ purpose }: { purpose: string }) => purpose !== ConsentConstants.DEFAULT_CONSENT @@ -283,7 +284,7 @@ export const Consents: FunctionComponent = (props: Consen }); }); - const allPurposesExceptDefault: PurposeModel[] = purposeDetailModels.filter( + const allPurposesExceptDefault: PurposeModel[] = purposeDetailModelsRef.current.filter( ({ purpose }: { purpose: string }) => purpose !== ConsentConstants.DEFAULT_CONSENT @@ -412,7 +413,7 @@ export const Consents: FunctionComponent = (props: Consen await attachResidentIDPReceiptMissingPurposes(receipt); await attachResidentIDPReceiptPurposes(receipt); } else { - const defaultPurpose: PurposeModel = purposeDetailModels.find( + const defaultPurpose: PurposeModel = purposeDetailModelsRef.current.find( ({ purpose }: { purpose: string }) => purpose === ConsentConstants.DEFAULT_CONSENT ); diff --git a/apps/myaccount/src/components/federated-associations/federated-associations.tsx b/apps/myaccount/src/components/federated-associations/federated-associations.tsx index 2bf279cc2a6..fa5d8835155 100644 --- a/apps/myaccount/src/components/federated-associations/federated-associations.tsx +++ b/apps/myaccount/src/components/federated-associations/federated-associations.tsx @@ -24,7 +24,7 @@ import { TestableComponentInterface, } from "@wso2is/core/models"; import { AppAvatar, Popup } from "@wso2is/react-components"; import { AxiosError } from "axios"; -import React, { FunctionComponent, ReactElement, useEffect, useState } from "react"; +import React, { FunctionComponent, MutableRefObject, ReactElement, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { Button, Grid, Icon, List, Modal } from "semantic-ui-react"; import { deleteFederatedAssociation, getFederatedAssociations } from "../../api/federated-associations"; @@ -69,7 +69,8 @@ export const FederatedAssociations: FunctionComponent = useRef(null); const { t } = useTranslation(); const [ federatedAssociations, setFederatedAssociations ] = useState([]); const [ showExternalLogins, setShowExternalLogins ] = useState(true); @@ -207,7 +208,7 @@ export const FederatedAssociations: FunctionComponent { - setId(null); + idRef.current = null; setConfirmDelete(false); } } > @@ -217,9 +218,9 @@ export const FederatedAssociations: FunctionComponent { removeFederatedAssociation( - id + idRef.current ); - setId(null); + idRef.current = null; setConfirmDelete(false); } } > @@ -292,7 +293,7 @@ export const FederatedAssociations: FunctionComponent { - setId(federatedAssociation.id); + idRef.current = federatedAssociation.id; setConfirmDelete(true); } } /> diff --git a/apps/myaccount/src/components/linked-accounts/linked-accounts-edit.tsx b/apps/myaccount/src/components/linked-accounts/linked-accounts-edit.tsx index ff12271ea2c..865d69d209a 100644 --- a/apps/myaccount/src/components/linked-accounts/linked-accounts-edit.tsx +++ b/apps/myaccount/src/components/linked-accounts/linked-accounts-edit.tsx @@ -18,7 +18,7 @@ import { TestableComponentInterface } from "@wso2is/core/models"; import { Field, FormValue, Forms } from "@wso2is/forms/legacy"; -import React, { FunctionComponent, ReactElement, useState } from "react"; +import React, { FunctionComponent, MutableRefObject, ReactElement, useRef } from "react"; import { useTranslation } from "react-i18next"; import { Form, Grid, Input, InputOnChangeData } from "semantic-ui-react"; import { UIConstants } from "../../constants"; @@ -59,7 +59,8 @@ export const LinkedAccountsEdit: FunctionComponent = ( const { t } = useTranslation(); - const [ userName, setUserName ] = useState(undefined); + // Captures the latest typed username; read only on submit, never rendered, so a ref avoids per-keystroke re-renders. + const userNameRef: MutableRefObject = useRef(undefined); /** * @@ -67,7 +68,7 @@ export const LinkedAccountsEdit: FunctionComponent = ( * @param data - Input field data. */ const handleUsernameChange = (event: React.ChangeEvent, data: InputOnChangeData): void => { - setUserName(data.value); + userNameRef.current = data.value; }; /** @@ -77,7 +78,7 @@ export const LinkedAccountsEdit: FunctionComponent = ( const getFormValues = (values: Map): void => { const formValues: { password: string; username: string } = { password: values.get("password").toString(), - username: userName + username: userNameRef.current }; onFormSubmit(formValues, UIConstants.ADD_LOCAL_LINKED_ACCOUNT_FORM_IDENTIFIER); diff --git a/apps/myaccount/src/components/linked-accounts/linked-accounts-list.tsx b/apps/myaccount/src/components/linked-accounts/linked-accounts-list.tsx index cb6a0762bf5..ccdedf775c3 100644 --- a/apps/myaccount/src/components/linked-accounts/linked-accounts-list.tsx +++ b/apps/myaccount/src/components/linked-accounts/linked-accounts-list.tsx @@ -18,7 +18,7 @@ import { TestableComponentInterface } from "@wso2is/core/models"; import { Popup } from "@wso2is/react-components"; -import React, { FunctionComponent, useState } from "react"; +import React, { FunctionComponent, MutableRefObject, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { Button, Grid, Icon, List, Modal } from "semantic-ui-react"; import { getGravatarImage } from "../../api"; @@ -54,7 +54,8 @@ export const LinkedAccountsList: FunctionComponent = ( ["data-testid"]: testId } = props; const [ confirmDelete, setConfirmDelete ] = useState(false); - const [ userID, setUserID ] = useState(null); + // Holds the account id targeted by the delete-confirmation modal; only read/written in handlers. + const userIDRef: MutableRefObject = useRef(null); const { t } = useTranslation(); @@ -77,7 +78,7 @@ export const LinkedAccountsList: FunctionComponent = (