From b243f1120ac59d745396e5b1af72824ca5ec1a83 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 24 Feb 2026 12:53:50 -0800 Subject: [PATCH 01/72] Fix React Compiler warning in Avatar Replace useEffect+setState with React's recommended "adjust state during rendering" pattern for resetting imageError when source changes. Co-authored-by: Cursor --- src/components/Avatar.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index 5b10a2b1e36c..59391ab1ead0 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useState} from 'react'; +import React, {useState} from 'react'; import type {ImageStyle, StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; import useDefaultAvatars from '@hooks/useDefaultAvatars'; @@ -78,13 +78,14 @@ function Avatar({ const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const [imageError, setImageError] = useState(false); + const [prevSource, setPrevSource] = useState(originalSource); + if (prevSource !== originalSource) { + setPrevSource(originalSource); + setImageError(false); + } useNetwork({onReconnect: () => setImageError(false)}); - useEffect(() => { - setImageError(false); - }, [originalSource]); - const isWorkspace = type === CONST.ICON_TYPE_WORKSPACE; const userAccountID = isWorkspace ? undefined : (avatarID as number); From f4442f830c83bda65470b31eb2e5a85c5fa1831c Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 24 Feb 2026 12:55:02 -0800 Subject: [PATCH 02/72] Fix React Compiler warning in ThumbnailImage Replace useEffect+setState with adjust-state-during-render pattern. Remove useCallback and React.memo since the file now compiles with React Compiler. Co-authored-by: Cursor --- src/components/ThumbnailImage.tsx | 47 +++++++++++++------------------ 1 file changed, 19 insertions(+), 28 deletions(-) diff --git a/src/components/ThumbnailImage.tsx b/src/components/ThumbnailImage.tsx index 8ab700150540..934fdefc14c5 100644 --- a/src/components/ThumbnailImage.tsx +++ b/src/components/ThumbnailImage.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useEffect, useState} from 'react'; +import React, {useState} from 'react'; import type {ImageResizeMode, ImageSourcePropType, StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; @@ -107,37 +107,28 @@ function ThumbnailImage({ const theme = useTheme(); const {isOffline} = useNetwork(); const [failedToLoad, setFailedToLoad] = useState(false); + const [prevLoadKey, setPrevLoadKey] = useState({isOffline, previewSourceURL}); + if (prevLoadKey.isOffline !== isOffline || prevLoadKey.previewSourceURL !== previewSourceURL) { + setPrevLoadKey({isOffline, previewSourceURL}); + setFailedToLoad(false); + } + const cachedDimensions = shouldDynamicallyResize && typeof previewSourceURL === 'string' ? thumbnailDimensionsCache.get(previewSourceURL) : null; const [imageDimensions, setImageDimensions] = useState({width: cachedDimensions?.width ?? imageWidth, height: cachedDimensions?.height ?? imageHeight}); const {thumbnailDimensionsStyles} = useThumbnailDimensions(imageDimensions.width, imageDimensions.height); const StyleUtils = useStyleUtils(); - useEffect(() => { - setFailedToLoad(false); - }, [isOffline, previewSourceURL]); - - /** - * Update the state with the computed thumbnail sizes. - * @param Params - width and height of the original image. - */ - const updateImageSize = useCallback( - ({width, height}: Dimensions) => { - if ( - !shouldDynamicallyResize || - // If the provided dimensions are good avoid caching them and updating state. - (imageDimensions.width === width && imageDimensions.height === height) - ) { - return; - } - - if (typeof previewSourceURL === 'string') { - thumbnailDimensionsCache.set(previewSourceURL, {width, height}); - } - - setImageDimensions({width, height}); - }, - [previewSourceURL, imageDimensions.width, imageDimensions.height, shouldDynamicallyResize], - ); + const updateImageSize = ({width, height}: Dimensions) => { + if (!shouldDynamicallyResize || (imageDimensions.width === width && imageDimensions.height === height)) { + return; + } + + if (typeof previewSourceURL === 'string') { + thumbnailDimensionsCache.set(previewSourceURL, {width, height}); + } + + setImageDimensions({width, height}); + }; const sizeStyles = shouldDynamicallyResize ? [thumbnailDimensionsStyles] : [styles.w100, styles.h100]; @@ -188,4 +179,4 @@ function ThumbnailImage({ ThumbnailImage.displayName = 'ThumbnailImage'; -export default React.memo(ThumbnailImage); +export default ThumbnailImage; From de2d12c51e83f5c2b16d705b9706b6496b4d6212 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 24 Feb 2026 12:56:51 -0800 Subject: [PATCH 03/72] Fix React Compiler warning in RadioButtons Use controlled/uncontrolled pattern instead of syncing prop to state via useEffect. When value prop is provided, use it directly. Co-authored-by: Cursor --- src/components/RadioButtons.tsx | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/components/RadioButtons.tsx b/src/components/RadioButtons.tsx index 9104a2572a4f..c6feaea71a70 100644 --- a/src/components/RadioButtons.tsx +++ b/src/components/RadioButtons.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useState} from 'react'; +import React, {useState} from 'react'; import type {ForwardedRef} from 'react'; import {View} from 'react-native'; import type {StyleProp, ViewStyle} from 'react-native'; @@ -41,14 +41,8 @@ type RadioButtonsProps = ForwardedFSClassProps & { function RadioButtons({items, onPress, defaultCheckedValue = '', radioButtonStyle, errorText, onInputChange = () => {}, value, forwardedFSClass, ref}: RadioButtonsProps) { const styles = useThemeStyles(); - const [checkedValue, setCheckedValue] = useState(defaultCheckedValue); - - useEffect(() => { - if (value === checkedValue || value === undefined) { - return; - } - setCheckedValue(value ?? ''); - }, [checkedValue, value]); + const [localValue, setLocalValue] = useState(defaultCheckedValue); + const checkedValue = value !== undefined ? value : localValue; return ( <> @@ -62,7 +56,7 @@ function RadioButtons({items, onPress, defaultCheckedValue = '', radioButtonStyl isChecked={item.value === checkedValue} style={[styles.mb4, radioButtonStyle]} onPress={() => { - setCheckedValue(item.value); + setLocalValue(item.value); onInputChange(item.value); return onPress(item.value); }} From 0be8e54612dcbe25efba54e9ebd9d35779dfc997 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 24 Feb 2026 12:56:52 -0800 Subject: [PATCH 04/72] Fix React Compiler warning in PlaidCardFeedIcon Replace useEffect+setState with adjust-state-during-render pattern. Remove useMemo since the file now compiles with React Compiler. Co-authored-by: Cursor --- src/components/PlaidCardFeedIcon.tsx | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/src/components/PlaidCardFeedIcon.tsx b/src/components/PlaidCardFeedIcon.tsx index 9732e2fa601a..a585de1cc20d 100644 --- a/src/components/PlaidCardFeedIcon.tsx +++ b/src/components/PlaidCardFeedIcon.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useMemo, useState} from 'react'; +import React, {useState} from 'react'; import {View} from 'react-native'; import type {StyleProp, ViewStyle} from 'react-native'; import {useCompanyCardBankIcons, useCompanyCardFeedIcons} from '@hooks/useCompanyCardIcons'; @@ -28,26 +28,22 @@ function PlaidCardFeedIcon({plaidUrl, style, isLarge, isSmall, useSkeletonLoader const width = isLarge ? variables.cardPreviewWidth : variables.cardIconWidth; const height = isLarge ? variables.cardPreviewHeight : variables.cardIconHeight; const [loading, setLoading] = useState(true); + const [prevPlaidUrl, setPrevPlaidUrl] = useState(plaidUrl); + if (prevPlaidUrl !== plaidUrl && plaidUrl) { + setPrevPlaidUrl(plaidUrl); + setIsBrokenImage(false); + setLoading(true); + } + const plaidImageStyle = isLarge ? styles.plaidIcon : styles.plaidIconSmall; const iconWidth = isSmall ? variables.cardMiniatureWidth : width; const iconHeight = isSmall ? variables.cardMiniatureHeight : height; const plaidLoadedStyle = isSmall ? styles.plaidIconExtraSmall : plaidImageStyle; - useEffect(() => { - if (!plaidUrl) { - return; - } - setIsBrokenImage(false); - setLoading(true); - }, [plaidUrl]); - - const reasonAttributes = useMemo( - () => ({ - context: 'PlaidCardFeedIcon', - loading, - }), - [loading], - ); + const reasonAttributes: SkeletonSpanReasonAttributes = { + context: 'PlaidCardFeedIcon', + loading, + }; return ( From ab0b41b3797e4e9e911e7e56c6f3d915d95d3e30 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 24 Feb 2026 12:56:52 -0800 Subject: [PATCH 05/72] Fix React Compiler warning in TermsStep Derive error visibility during render instead of clearing error state in a useEffect. The error is shown only when both checkboxes are not yet accepted, matching the original behavior without the effect. Co-authored-by: Cursor --- src/pages/EnablePayments/TermsStep.tsx | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/pages/EnablePayments/TermsStep.tsx b/src/pages/EnablePayments/TermsStep.tsx index 9a2aae858ef1..a4927b776d0a 100644 --- a/src/pages/EnablePayments/TermsStep.tsx +++ b/src/pages/EnablePayments/TermsStep.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useState} from 'react'; +import React, {useState} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import CheckboxWithLabel from '@components/CheckboxWithLabel'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; @@ -45,7 +45,8 @@ function TermsStep(props: TermsStepProps) { const [error, setError] = useState(false); const {translate} = useLocalize(); const [walletTerms] = useOnyx(ONYXKEYS.WALLET_TERMS); - const errorMessage = error ? translate('common.error.acceptTerms') : (getLatestErrorMessage(walletTerms ?? {}) ?? ''); + const shouldShowError = error && (!hasAcceptedDisclosure || !hasAcceptedPrivacyPolicyAndWalletAgreement); + const errorMessage = shouldShowError ? translate('common.error.acceptTerms') : (getLatestErrorMessage(walletTerms ?? {}) ?? ''); const toggleDisclosure = () => { setHasAcceptedDisclosure(!hasAcceptedDisclosure); @@ -55,15 +56,6 @@ function TermsStep(props: TermsStepProps) { setHasAcceptedPrivacyPolicyAndWalletAgreement(!hasAcceptedPrivacyPolicyAndWalletAgreement); }; - /** clear error */ - useEffect(() => { - if (!hasAcceptedDisclosure || !hasAcceptedPrivacyPolicyAndWalletAgreement) { - return; - } - - setError(false); - }, [hasAcceptedDisclosure, hasAcceptedPrivacyPolicyAndWalletAgreement]); - return ( <> @@ -101,7 +93,7 @@ function TermsStep(props: TermsStepProps) { }); }} message={errorMessage} - isAlertVisible={error || !!errorMessage} + isAlertVisible={shouldShowError || !!errorMessage} isLoading={!!walletTerms?.isLoading} containerStyles={[styles.mh0, styles.mv4]} /> From f1ff5f9959eb5ee5d1c9fc23bf4cbd26bda4a6e3 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 24 Feb 2026 12:56:52 -0800 Subject: [PATCH 06/72] Fix React Compiler warning in FreeTrial Compute freeTrialText inline during render instead of storing it in state and updating via useEffect. Remove useState since the value is purely derived from props and Onyx data. Co-authored-by: Cursor --- src/pages/settings/Subscription/FreeTrial.tsx | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/pages/settings/Subscription/FreeTrial.tsx b/src/pages/settings/Subscription/FreeTrial.tsx index c545f756d3aa..b77646b79176 100644 --- a/src/pages/settings/Subscription/FreeTrial.tsx +++ b/src/pages/settings/Subscription/FreeTrial.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useState} from 'react'; +import React from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; import Badge from '@components/Badge'; @@ -30,16 +30,10 @@ function FreeTrial({badgeStyles, pressable = false, addSpacing = false, success const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); const privateSubscription = usePrivateSubscription(); - const [freeTrialText, setFreeTrialText] = useState(undefined); const {isOffline} = useNetwork(); const {translate} = useLocalize(); - useEffect(() => { - if (!privateSubscription && !isOffline) { - return; - } - setFreeTrialText(getFreeTrialText(translate, policies, introSelected, firstDayFreeTrial, lastDayFreeTrial)); - }, [isOffline, privateSubscription, translate, policies, firstDayFreeTrial, lastDayFreeTrial, introSelected]); + const freeTrialText = privateSubscription || isOffline ? getFreeTrialText(translate, policies, introSelected, firstDayFreeTrial, lastDayFreeTrial) : undefined; if (!freeTrialText) { return null; From 2d0b782bb003f738005922838b98b75ac29cef34 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 24 Feb 2026 13:00:38 -0800 Subject: [PATCH 07/72] Fix React Compiler warning in NewTaskDetailsPage Replace useEffect+setState with adjust-state-during-render pattern for syncing task title and description from Onyx. Remove useMemo wrappers since the file now compiles with React Compiler. Co-authored-by: Cursor --- src/pages/tasks/NewTaskDetailsPage.tsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/pages/tasks/NewTaskDetailsPage.tsx b/src/pages/tasks/NewTaskDetailsPage.tsx index 1314c5ed038e..f196d9a4313c 100644 --- a/src/pages/tasks/NewTaskDetailsPage.tsx +++ b/src/pages/tasks/NewTaskDetailsPage.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useMemo, useState} from 'react'; +import React, {useState} from 'react'; import {View} from 'react-native'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; @@ -38,19 +38,21 @@ function NewTaskDetailsPage({route}: NewTaskDetailsPageProps) { const {translate} = useLocalize(); const [taskTitle, setTaskTitle] = useState(task?.title ?? ''); const [taskDescription, setTaskDescription] = useState(task?.description ?? ''); - const titleDefaultValue = useMemo(() => Parser.htmlToMarkdown(Parser.replace(taskTitle)), [taskTitle]); - const descriptionDefaultValue = useMemo(() => Parser.htmlToMarkdown(Parser.replace(taskDescription)), [taskDescription]); + const [prevTask, setPrevTask] = useState({title: task?.title, description: task?.description}); + if (prevTask.title !== task?.title || prevTask.description !== task?.description) { + setPrevTask({title: task?.title, description: task?.description}); + setTaskTitle(Parser.htmlToMarkdown(Parser.replace(task?.title ?? ''))); + setTaskDescription(Parser.htmlToMarkdown(Parser.replace(task?.description ?? ''))); + } + + const titleDefaultValue = Parser.htmlToMarkdown(Parser.replace(taskTitle)); + const descriptionDefaultValue = Parser.htmlToMarkdown(Parser.replace(taskDescription)); const {inputCallbackRef} = useAutoFocusInput(); const backTo = route.params?.backTo; const skipConfirmation = task?.skipConfirmation && task?.assigneeAccountID && task?.parentReportID; const buttonText = skipConfirmation ? translate('newTaskPage.assignTask') : translate('common.next'); - useEffect(() => { - setTaskTitle(Parser.htmlToMarkdown(Parser.replace(task?.title ?? ''))); - setTaskDescription(Parser.htmlToMarkdown(Parser.replace(task?.description ?? ''))); - }, [task?.title, task?.description]); - const validate = (values: FormOnyxValues): FormInputErrors => { const errors = {}; From d20dbcc754159534cb9fd483b8ba1ba0e05bb9dd Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 24 Feb 2026 13:00:38 -0800 Subject: [PATCH 08/72] Fix React Compiler warning in NewTaskPage Move error message clearing from useEffect to render-time using a composite key of task fields. Narrow the remaining effect to only handle parentReportID changes. Co-authored-by: Cursor --- src/pages/tasks/NewTaskPage.tsx | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/pages/tasks/NewTaskPage.tsx b/src/pages/tasks/NewTaskPage.tsx index 7cec92ceb3c3..c5c7c6562965 100644 --- a/src/pages/tasks/NewTaskPage.tsx +++ b/src/pages/tasks/NewTaskPage.tsx @@ -49,6 +49,13 @@ function NewTaskPage({route}: NewTaskPageProps) { const parentReport = task?.shareDestination ? reports?.[`${ONYXKEYS.COLLECTION.REPORT}${task.shareDestination}`] : undefined; const ancestors = useAncestors(parentReport); const [errorMessage, setErrorMessage] = useState(''); + const taskKey = `${task?.assignee}|${task?.assigneeAccountID}|${task?.description}|${task?.parentReportID}|${task?.shareDestination}|${task?.title}`; + const [prevTaskKey, setPrevTaskKey] = useState(taskKey); + if (prevTaskKey !== taskKey) { + setPrevTaskKey(taskKey); + setErrorMessage(''); + } + const hasDestinationError = task?.skipConfirmation && !task?.parentReportID; const isAllowedToCreateTask = isEmptyObject(parentReport) || isAllowedToComment(parentReport); @@ -68,15 +75,11 @@ function NewTaskPage({route}: NewTaskPageProps) { }); useEffect(() => { - setErrorMessage(''); - - // We only set the parentReportID if we are creating a task from a report - // this allows us to go ahead and set that report as the share destination - // and disable the share destination selector - if (task?.parentReportID) { - setShareDestinationValue(task.parentReportID); + if (!task?.parentReportID) { + return; } - }, [task?.assignee, task?.assigneeAccountID, task?.description, task?.parentReportID, task?.shareDestination, task?.title]); + setShareDestinationValue(task.parentReportID); + }, [task?.parentReportID]); // On submit, we want to call the createTask function and wait to validate // the response From 6fe38a879b898157019e5dafbd5c113caf7c70ab Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 24 Feb 2026 13:00:38 -0800 Subject: [PATCH 09/72] Fix React Compiler warning in AutoUpdateTime Move the synchronous time update from useEffect to render-time using prev-timezone tracking. Keep only the interval in the effect. Remove useCallback and useMemo since the file now compiles with React Compiler. Co-authored-by: Cursor --- src/components/AutoUpdateTime.tsx | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/src/components/AutoUpdateTime.tsx b/src/components/AutoUpdateTime.tsx index 64331226f63c..815921b1b419 100644 --- a/src/components/AutoUpdateTime.tsx +++ b/src/components/AutoUpdateTime.tsx @@ -2,7 +2,7 @@ * Displays the user's local time and updates it every minute. * The time auto-update logic is extracted to this component to avoid re-rendering a more complex component, e.g. DetailsPage. */ -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {useEffect, useRef, useState} from 'react'; import {View} from 'react-native'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -18,33 +18,28 @@ type AutoUpdateTimeProps = { function AutoUpdateTime({timezone}: AutoUpdateTimeProps) { const {translate, getLocalDateFromDatetime} = useLocalize(); const styles = useThemeStyles(); - /** @returns Returns the locale Date object */ - const getCurrentUserLocalTime = useCallback(() => getLocalDateFromDatetime(undefined, timezone.selected), [getLocalDateFromDatetime, timezone.selected]); + const getCurrentUserLocalTime = () => getLocalDateFromDatetime(undefined, timezone.selected); const [currentUserLocalTime, setCurrentUserLocalTime] = useState(getCurrentUserLocalTime); + const [prevTimezone, setPrevTimezone] = useState(timezone.selected); + if (prevTimezone !== timezone.selected) { + setPrevTimezone(timezone.selected); + setCurrentUserLocalTime(getCurrentUserLocalTime()); + } + const minuteRef = useRef(new Date().getMinutes()); - const timezoneName = useMemo(() => { - if (timezone.selected) { - return DateUtils.getZoneAbbreviation(currentUserLocalTime, timezone.selected); - } - return ''; - }, [currentUserLocalTime, timezone.selected]); + const timezoneName = timezone.selected ? DateUtils.getZoneAbbreviation(currentUserLocalTime, timezone.selected) : ''; useEffect(() => { - // If any of the props that getCurrentUserLocalTime depends on change, we want to update the displayed time immediately - setCurrentUserLocalTime(getCurrentUserLocalTime()); - - // Also, if the user leaves this page open, we want to make sure the displayed time is updated every minute when the clock changes - // To do this we create an interval to check if the minute has changed every second and update the displayed time if it has const interval = setInterval(() => { const currentMinute = new Date().getMinutes(); if (currentMinute !== minuteRef.current) { - setCurrentUserLocalTime(getCurrentUserLocalTime()); + setCurrentUserLocalTime(getLocalDateFromDatetime(undefined, timezone.selected)); minuteRef.current = currentMinute; } }, 1000); return () => clearInterval(interval); - }, [getCurrentUserLocalTime]); + }, [getLocalDateFromDatetime, timezone.selected]); return ( From 3eef7c60acb92326386435ea90569b6ee93020ff Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 24 Feb 2026 13:00:39 -0800 Subject: [PATCH 10/72] Fix React Compiler warning in TransactionStartDateStep Replace useEffect+setState with adjust-state-during-render pattern for syncing dateOption and startDate from Onyx card data. Co-authored-by: Cursor --- .../assignCard/TransactionStartDateStep.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pages/workspace/companyCards/assignCard/TransactionStartDateStep.tsx b/src/pages/workspace/companyCards/assignCard/TransactionStartDateStep.tsx index 210e248b68d1..4758cd30916f 100644 --- a/src/pages/workspace/companyCards/assignCard/TransactionStartDateStep.tsx +++ b/src/pages/workspace/companyCards/assignCard/TransactionStartDateStep.tsx @@ -1,5 +1,5 @@ import {format, subDays} from 'date-fns'; -import React, {useEffect, useState} from 'react'; +import React, {useState} from 'react'; import {View} from 'react-native'; import ActivityIndicator from '@components/ActivityIndicator'; import Button from '@components/Button'; @@ -29,16 +29,16 @@ function TransactionStartDateStep() { const [dateOptionSelected, setDateOptionSelected] = useState(cardToAssign?.dateOption ?? CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM); const [errorText, setErrorText] = useState(''); const [startDate, setStartDate] = useState(() => assignCard?.startDate ?? cardToAssign?.startDate ?? format(new Date(), CONST.DATE.FNS_FORMAT_STRING)); - - useEffect(() => { + const [prevCardToAssign, setPrevCardToAssign] = useState({dateOption: cardToAssign?.dateOption, startDate: cardToAssign?.startDate}); + if (prevCardToAssign.dateOption !== cardToAssign?.dateOption || prevCardToAssign.startDate !== cardToAssign?.startDate) { + setPrevCardToAssign({dateOption: cardToAssign?.dateOption, startDate: cardToAssign?.startDate}); if (cardToAssign?.dateOption) { setDateOptionSelected(cardToAssign.dateOption); } - if (cardToAssign?.startDate) { setStartDate(cardToAssign.startDate); } - }, [cardToAssign?.dateOption, cardToAssign?.startDate]); + } const handleBackButtonPress = () => { if (isEditing) { From 329eef9d47d9314a16f0660e7de02ca21b80303b Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 24 Feb 2026 13:00:39 -0800 Subject: [PATCH 11/72] Fix React Compiler warning in AmexCustomFeed Replace useEffect+setState with adjust-state-during-render pattern. Remove useCallback and useMemo since the file now compiles with React Compiler. Co-authored-by: Cursor --- .../companyCards/addNew/AmexCustomFeed.tsx | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/src/pages/workspace/companyCards/addNew/AmexCustomFeed.tsx b/src/pages/workspace/companyCards/addNew/AmexCustomFeed.tsx index 3015a26e136a..7fea3771807f 100644 --- a/src/pages/workspace/companyCards/addNew/AmexCustomFeed.tsx +++ b/src/pages/workspace/companyCards/addNew/AmexCustomFeed.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useEffect, useMemo, useState} from 'react'; +import React, {useState} from 'react'; import {View} from 'react-native'; import type {ValueOf} from 'type-fest'; import FormHelpMessage from '@components/FormHelpMessage'; @@ -19,10 +19,16 @@ function AmexCustomFeed() { const {translate} = useLocalize(); const styles = useThemeStyles(); const [addNewCard] = useOnyx(ONYXKEYS.ADD_NEW_COMPANY_CARD); - const [typeSelected, setTypeSelected] = useState>(); + const [typeSelected, setTypeSelected] = useState>(addNewCard?.data.selectedAmexCustomFeed); + const [prevSelectedFeed, setPrevSelectedFeed] = useState(addNewCard?.data.selectedAmexCustomFeed); + if (prevSelectedFeed !== addNewCard?.data.selectedAmexCustomFeed) { + setPrevSelectedFeed(addNewCard?.data.selectedAmexCustomFeed); + setTypeSelected(addNewCard?.data.selectedAmexCustomFeed); + } + const [hasError, setHasError] = useState(false); - const submit = useCallback(() => { + const submit = () => { if (!typeSelected) { setHasError(true); return; @@ -34,11 +40,7 @@ function AmexCustomFeed() { selectedAmexCustomFeed: typeSelected, }, }); - }, [typeSelected]); - - useEffect(() => { - setTypeSelected(addNewCard?.data.selectedAmexCustomFeed); - }, [addNewCard?.data.selectedAmexCustomFeed]); + }; const handleBackButtonPress = () => { setAddNewCompanyCardStepAndData({step: CONST.COMPANY_CARDS.STEP.SELECT_BANK}); @@ -68,14 +70,11 @@ function AmexCustomFeed() { }, ]; - const confirmButtonOptions = useMemo( - () => ({ - showButton: true, - text: translate('common.next'), - onConfirm: submit, - }), - [submit, translate], - ); + const confirmButtonOptions = { + showButton: true, + text: translate('common.next'), + onConfirm: submit, + }; return ( Date: Tue, 24 Feb 2026 13:03:06 -0800 Subject: [PATCH 12/72] Fix React Compiler warning in TextInputWithSymbol (Android) Replace useEffect+setState with adjust-state-during-render pattern for setting the skip-selection-change flag when formattedAmount changes. Co-authored-by: Cursor --- src/components/TextInputWithSymbol/index.android.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/TextInputWithSymbol/index.android.tsx b/src/components/TextInputWithSymbol/index.android.tsx index 1950f2831daf..e211c5a01898 100644 --- a/src/components/TextInputWithSymbol/index.android.tsx +++ b/src/components/TextInputWithSymbol/index.android.tsx @@ -1,14 +1,15 @@ -import React, {useEffect, useState} from 'react'; +import React, {useState} from 'react'; import type {TextInputSelectionChangeEvent} from 'react-native'; import BaseTextInputWithSymbol from './BaseTextInputWithSymbol'; import type {TextInputWithSymbolProps} from './types'; function TextInputWithSymbol({onSelectionChange = () => {}, ref, ...props}: TextInputWithSymbolProps) { const [skipNextSelectionChange, setSkipNextSelectionChange] = useState(false); - - useEffect(() => { + const [prevFormattedAmount, setPrevFormattedAmount] = useState(props.formattedAmount); + if (prevFormattedAmount !== props.formattedAmount) { + setPrevFormattedAmount(props.formattedAmount); setSkipNextSelectionChange(true); - }, [props.formattedAmount]); + } return ( Date: Tue, 24 Feb 2026 13:03:07 -0800 Subject: [PATCH 13/72] Fix React Compiler warning in AnimatedCollapsible Replace useEffect+setState with adjust-state-during-render pattern for updating isRendered and shared value when isExpanded changes. Co-authored-by: Cursor --- src/components/AnimatedCollapsible/index.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/AnimatedCollapsible/index.tsx b/src/components/AnimatedCollapsible/index.tsx index 805922b0db28..718dfb00e5dd 100644 --- a/src/components/AnimatedCollapsible/index.tsx +++ b/src/components/AnimatedCollapsible/index.tsx @@ -1,4 +1,4 @@ -import React, {useEffect} from 'react'; +import React from 'react'; import type {ReactNode} from 'react'; import {View} from 'react-native'; import type {StyleProp, ViewStyle} from 'react-native'; @@ -77,12 +77,14 @@ function AnimatedCollapsible({ const descriptionHeight = useSharedValue(0); const hasExpanded = useSharedValue(isExpanded); const [isRendered, setIsRendered] = React.useState(isExpanded); - useEffect(() => { + const [prevExpanded, setPrevExpanded] = React.useState(isExpanded); + if (prevExpanded !== isExpanded) { + setPrevExpanded(isExpanded); hasExpanded.set(isExpanded); if (isExpanded) { setIsRendered(true); } - }, [isExpanded, hasExpanded]); + } const animatedHeight = useDerivedValue(() => { if (!contentHeight.get()) { From 795142dcf1a63af2c8b2733f6c3e80a71a32f537 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 24 Feb 2026 13:03:07 -0800 Subject: [PATCH 14/72] Fix React Compiler warning in useFilteredSelection Replace useEffect+setState with adjust-state-during-render pattern for filtering selected options when options or filter function change. Co-authored-by: Cursor --- src/hooks/useFilteredSelection.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/hooks/useFilteredSelection.ts b/src/hooks/useFilteredSelection.ts index e803bdd61e7c..2dfe472db932 100644 --- a/src/hooks/useFilteredSelection.ts +++ b/src/hooks/useFilteredSelection.ts @@ -1,4 +1,4 @@ -import {useEffect, useState} from 'react'; +import {useState} from 'react'; /** * Custom hook to manage a selection of keys from a given set of options. @@ -10,8 +10,11 @@ import {useEffect, useState} from 'react'; */ function useFilteredSelection(options: Record | undefined, filter: (option: TValue | undefined) => boolean) { const [selectedOptions, setSelectedOptions] = useState([]); - - useEffect(() => setSelectedOptions((prevOptions) => prevOptions.filter((key) => filter(options?.[key]))), [options, filter]); + const [prevDeps, setPrevDeps] = useState({options, filter}); + if (prevDeps.options !== options || prevDeps.filter !== filter) { + setPrevDeps({options, filter}); + setSelectedOptions((prev) => prev.filter((key) => filter(options?.[key]))); + } return [selectedOptions, setSelectedOptions] as const; } From 2d89602af0797904133538b6b7f598eb74d8b177 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 24 Feb 2026 13:03:07 -0800 Subject: [PATCH 15/72] Fix React Compiler warning in ReportCardLostConfirmMagicCodePage Replace useEffect+setState with adjust-state-during-render pattern for detecting new card IDs. Remove useCallback since the file now compiles with React Compiler. Co-authored-by: Cursor --- .../ReportCardLostConfirmMagicCodePage.tsx | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/pages/settings/Wallet/ReportCardLostConfirmMagicCodePage.tsx b/src/pages/settings/Wallet/ReportCardLostConfirmMagicCodePage.tsx index 8a34e9cabf19..e363105cb7f8 100644 --- a/src/pages/settings/Wallet/ReportCardLostConfirmMagicCodePage.tsx +++ b/src/pages/settings/Wallet/ReportCardLostConfirmMagicCodePage.tsx @@ -1,5 +1,5 @@ import {deepEqual} from 'fast-equals'; -import React, {useCallback, useEffect, useState} from 'react'; +import React, {useEffect, useState} from 'react'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import ValidateCodeActionContent from '@components/ValidateCodeActionModal/ValidateCodeActionContent'; @@ -36,13 +36,14 @@ function ReportCardLostConfirmMagicCodePage({ const previousCardList = usePrevious(cardList); const validateError = getLatestErrorMessageField(physicalCard); - useEffect(() => { + const [prevCardList, setPrevCardList] = useState(cardList); + if (prevCardList !== cardList) { + setPrevCardList(cardList); const newID = Object.keys(cardList ?? {}).find((cardKey) => cardList?.[cardKey]?.cardID && !(cardKey in (previousCardList ?? {}))); - if (!newID || physicalCard?.cardID) { - return; + if (newID && !physicalCard?.cardID) { + setNewCardID(newID); } - setNewCardID(newID); - }, [cardList, physicalCard?.cardID, previousCardList]); + } useEffect(() => { if (formData?.isLoading) { @@ -57,15 +58,12 @@ function ReportCardLostConfirmMagicCodePage({ setErrors(ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM, newErrors); }, [formData?.isLoading, formData?.errors, physicalCard?.errors]); - const handleValidateCodeEntered = useCallback( - (validateCode: string) => { - if (!physicalCard) { - return; - } - requestReplacementExpensifyCard(physicalCard.cardID, reason, validateCode); - }, - [physicalCard, reason], - ); + const handleValidateCodeEntered = (validateCode: string) => { + if (!physicalCard) { + return; + } + requestReplacementExpensifyCard(physicalCard.cardID, reason, validateCode); + }; if (newCardID) { return ( From f72e00acd6d48af1ef03679c823777fed584458f Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 24 Feb 2026 13:04:48 -0800 Subject: [PATCH 16/72] Fix React Compiler warnings in VideoPlayerPreview Move synchronous dimension sync from useEffect to render-time for non-web platforms. Replace playback state useEffect+setState with adjust-state-during-render pattern for thumbnail toggling. Co-authored-by: Cursor --- src/components/VideoPlayerPreview/index.tsx | 58 +++++++++++---------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/src/components/VideoPlayerPreview/index.tsx b/src/components/VideoPlayerPreview/index.tsx index b63bacc6bfcd..ef15b1c49698 100644 --- a/src/components/VideoPlayerPreview/index.tsx +++ b/src/components/VideoPlayerPreview/index.tsx @@ -61,35 +61,35 @@ function VideoPlayerPreview({videoUrl, thumbnailUrl, reportID, fileName, videoDi const [isThumbnail, setIsThumbnail] = useState(true); const [measuredDimensions, setMeasuredDimensions] = useState(videoDimensions); + const [prevVideoDimensions, setPrevVideoDimensions] = useState(videoDimensions); + if (prevVideoDimensions !== videoDimensions && (getPlatform() !== CONST.PLATFORM.WEB || !videoUrl)) { + setPrevVideoDimensions(videoDimensions); + setMeasuredDimensions(videoDimensions); + } const {thumbnailDimensionsStyles} = useThumbnailDimensions(measuredDimensions.width, measuredDimensions.height); const isOnSearch = useIsOnSearch(); const navigation = useNavigation(); useEffect(() => { - const platform = getPlatform(); - // On web platform, we can use the DOM video element to get accurate video dimensions - // by loading the video metadata. On mobile platforms, we rely on the provided videoDimensions - // since document.createElement is not available in React Native environments. - if (videoUrl && platform === CONST.PLATFORM.WEB) { - const video = document.createElement('video'); - video.onloadedmetadata = () => { - if (video.videoWidth === measuredDimensions.width && video.videoHeight === measuredDimensions.height) { - return; - } - setMeasuredDimensions({ - width: video.videoWidth, - height: video.videoHeight, - }); - }; - video.src = videoUrl; - video.load(); - - return () => { - video.src = ''; - }; + if (!videoUrl || getPlatform() !== CONST.PLATFORM.WEB) { + return; } - setMeasuredDimensions(videoDimensions); - }, [videoUrl, measuredDimensions.width, measuredDimensions.height, videoDimensions]); + const video = document.createElement('video'); + video.onloadedmetadata = () => { + if (video.videoWidth === measuredDimensions.width && video.videoHeight === measuredDimensions.height) { + return; + } + setMeasuredDimensions({ + width: video.videoWidth, + height: video.videoHeight, + }); + }; + video.src = videoUrl; + video.load(); + return () => { + video.src = ''; + }; + }, [videoUrl, measuredDimensions.width, measuredDimensions.height]); // We want to play the video only when the user is on the page where it was initially rendered const doesUserRemainOnFirstRenderRoute = useCheckIfRouteHasRemainedUnchanged(videoUrl); @@ -117,13 +117,15 @@ function VideoPlayerPreview({videoUrl, thumbnailUrl, reportID, fileName, videoDi return navigation.addListener('blur', () => !isOnAttachmentRoute() && setIsThumbnail(true)); }, [navigation]); - useEffect(() => { + const playbackKey = `${currentlyPlayingURL}|${currentRouteReportID}|${videoUrl}|${reportID}|${isOnSearch}`; + const [prevPlaybackKey, setPrevPlaybackKey] = useState(playbackKey); + if (prevPlaybackKey !== playbackKey) { + setPrevPlaybackKey(playbackKey); const isFocused = doesUserRemainOnFirstRenderRoute(); - if (videoUrl !== currentlyPlayingURL || reportID !== currentRouteReportID || !isFocused) { - return; + if (videoUrl === currentlyPlayingURL && reportID === currentRouteReportID && isFocused) { + setIsThumbnail(false); } - setIsThumbnail(false); - }, [currentlyPlayingURL, currentRouteReportID, updateCurrentURLAndReportID, videoUrl, reportID, doesUserRemainOnFirstRenderRoute, isOnSearch]); + } return ( From 6e96db47131e956fd2f57a628775dd7b25abfce6 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 24 Feb 2026 13:06:50 -0800 Subject: [PATCH 17/72] Fix React Compiler warning in TabSelectorItem Move the synchronous desktop shift reset from useLayoutEffect to render-time. Keep async measurement in useLayoutEffect for mobile. Remove useMemo since the file now compiles with React Compiler. Co-authored-by: Cursor --- .../TabSelector/TabSelectorItem.tsx | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/src/components/TabSelector/TabSelectorItem.tsx b/src/components/TabSelector/TabSelectorItem.tsx index 5c4d1c7edb93..eb99889bd0e1 100644 --- a/src/components/TabSelector/TabSelectorItem.tsx +++ b/src/components/TabSelector/TabSelectorItem.tsx @@ -1,4 +1,4 @@ -import React, {useLayoutEffect, useMemo, useRef, useState} from 'react'; +import React, {useLayoutEffect, useRef, useState} from 'react'; // eslint-disable-next-line no-restricted-imports import {Animated} from 'react-native'; import type {View} from 'react-native'; @@ -48,36 +48,33 @@ function TabSelectorItem({ // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {isSmallScreenWidth} = useResponsiveLayout(); - // Compute horizontal shift for EducationalTooltip: - // - on desktop, ignore RHP bounds and center tooltip on the tab (no shift needed) - // - on mobile (aka small screen) center tooltip within the panel - useLayoutEffect(() => { - // only active tab gets tooltip - if (!isActive) { - return; + const layoutKey = `${isActive}|${isSmallScreenWidth}|${parentX}|${parentWidth}`; + const [prevLayoutKey, setPrevLayoutKey] = useState(layoutKey); + if (prevLayoutKey !== layoutKey) { + setPrevLayoutKey(layoutKey); + if (isActive && !isSmallScreenWidth) { + setShiftHorizontal(0); } + } - if (!isSmallScreenWidth) { - // no shift needed on desktop (note: not "shouldUseNarrowLayout") - setShiftHorizontal(0); + useLayoutEffect(() => { + if (!isActive || !isSmallScreenWidth) { return; } - // must allow animation to complete before taking measurement const timerID = setTimeout(() => { childRef.current?.measureInWindow((x, _y, width) => { - // To center tooltip in parent: - const parentCenter = parentX + parentWidth / 2; // ... where it should be... - const currentCenter = x + width / 2; // ... minus where it is now... - setShiftHorizontal(parentCenter - currentCenter); // ...equals the shift needed + const parentCenter = parentX + parentWidth / 2; + const currentCenter = x + width / 2; + setShiftHorizontal(parentCenter - currentCenter); }); }, CONST.TOOLTIP_ANIMATION_DURATION); return () => { clearTimeout(timerID); }; - }, [isActive, childRef, isSmallScreenWidth, parentX, parentWidth]); + }, [isActive, isSmallScreenWidth, parentX, parentWidth]); - const accessibilityState = useMemo(() => ({selected: isActive}), [isActive]); + const accessibilityState = {selected: isActive}; const children = ( Date: Tue, 24 Feb 2026 13:09:36 -0800 Subject: [PATCH 18/72] Fix React Compiler warning in CountryFullStep Replace useEffect+setState with adjust-state-during-render pattern for initializing userSelectedCountry from countryDefaultValue. Co-authored-by: Cursor --- src/components/SubStepForms/CountryFullStep.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/SubStepForms/CountryFullStep.tsx b/src/components/SubStepForms/CountryFullStep.tsx index 346a83792146..d45f090d5e76 100644 --- a/src/components/SubStepForms/CountryFullStep.tsx +++ b/src/components/SubStepForms/CountryFullStep.tsx @@ -98,13 +98,13 @@ function CountryFullStep({onBackButtonPress, stepNames, onSubmit, policyID, isCo onBackButtonPress(); }; - useEffect(() => { - if (selectedCountry || !countryDefaultValue) { - return; + const [prevCountryDefault, setPrevCountryDefault] = useState(countryDefaultValue); + if (prevCountryDefault !== countryDefaultValue) { + setPrevCountryDefault(countryDefaultValue); + if (!selectedCountry && countryDefaultValue) { + setUserSelectedCountry(countryDefaultValue); } - - setUserSelectedCountry(countryDefaultValue); - }, [selectedCountry, countryDefaultValue]); + } return ( Date: Tue, 24 Feb 2026 13:09:37 -0800 Subject: [PATCH 19/72] Fix React Compiler warnings in AnimatedSettlementButton Move animation reset state updates from useEffect to render-time. Replace DOM measurement in effect with a callback ref that measures width when the element mounts. Keep only the delayed hide timer in the effect. Co-authored-by: Cursor --- .../AnimatedSettlementButton.tsx | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/components/SettlementButton/AnimatedSettlementButton.tsx b/src/components/SettlementButton/AnimatedSettlementButton.tsx index 0b7f3a11fcef..9680d0f7029f 100644 --- a/src/components/SettlementButton/AnimatedSettlementButton.tsx +++ b/src/components/SettlementButton/AnimatedSettlementButton.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useEffect, useMemo, useRef} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import type {View} from 'react-native'; import Animated, {Keyframe, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import {scheduleOnRN} from 'react-native-worklets'; @@ -88,26 +88,40 @@ function AnimatedSettlementButton({ icon = expensifyIcons.Checkmark; } - useEffect(() => { + const [prevIsAnimationRunning, setPrevIsAnimationRunning] = useState(isAnimationRunning); + if (prevIsAnimationRunning !== isAnimationRunning) { + setPrevIsAnimationRunning(isAnimationRunning); if (!isAnimationRunning) { setMinWidth(0); setCanShow(true); height.set(variables.componentSizeNormal); buttonMarginTop.set(shouldAddTopMargin ? gap : 0); + } + } + + const animatedViewRef = useCallback( + (el: View | null) => { + viewRef.current = el as HTMLElement | null; + if (el && isAnimationRunning) { + setMinWidth((el as HTMLElement).getBoundingClientRect?.().width ?? 0); + } + }, + [isAnimationRunning], + ); + + useEffect(() => { + if (!isAnimationRunning) { return; } - setMinWidth(viewRef.current?.getBoundingClientRect?.().width ?? 0); const timer = setTimeout(() => setCanShow(false), CONST.ANIMATION_PAID_BUTTON_HIDE_DELAY); return () => clearTimeout(timer); - }, [buttonMarginTop, gap, height, isAnimationRunning, shouldAddTopMargin]); + }, [isAnimationRunning]); return ( {isAnimationRunning && canShow && ( { - viewRef.current = el as HTMLElement | null; - }} + ref={animatedViewRef} exiting={buttonAnimation} >