diff --git a/.eslintrc.js b/.eslintrc.js index 45d024c1566ed..cb5b65818c58e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -87,6 +87,15 @@ const restrictedImportPaths = [ importNames: ['memoize'], message: "Please use '@src/libs/memoize' instead.", }, + { + name: 'lodash/isEqual', + message: "Please use 'deepEqual' from 'fast-equals' instead.", + }, + { + name: 'lodash', + importNames: ['isEqual'], + message: "Please use 'deepEqual' from 'fast-equals' instead.", + }, { name: 'react-native-animatable', message: "Please use 'react-native-reanimated' instead.", diff --git a/src/components/Attachments/AttachmentCarousel/index.tsx b/src/components/Attachments/AttachmentCarousel/index.tsx index 3c883530df4f7..b3d81544a1bd5 100644 --- a/src/components/Attachments/AttachmentCarousel/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/index.tsx @@ -1,4 +1,4 @@ -import isEqual from 'lodash/isEqual'; +import {deepEqual} from 'fast-equals'; import type {MutableRefObject} from 'react'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import type {ListRenderItemInfo} from 'react-native'; @@ -100,7 +100,7 @@ function AttachmentCarousel({report, attachmentID, source, onNavigate, setDownlo newAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.REPORT, {parentReportAction, reportActions: reportActions ?? undefined, report}); } - if (isEqual(attachments, newAttachments)) { + if (deepEqual(attachments, newAttachments)) { if (attachments.length === 0) { setPage(-1); setDownloadButtonVisibility?.(false); diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index 319ddd2731b03..f778dde4d02e3 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -1,5 +1,5 @@ import {useFocusEffect} from '@react-navigation/native'; -import lodashIsEqual from 'lodash/isEqual'; +import {deepEqual} from 'fast-equals'; import type {ForwardedRef, MutableRefObject, ReactNode, RefAttributes} from 'react'; import React, {createRef, forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; import {InteractionManager} from 'react-native'; @@ -194,7 +194,7 @@ function FormProvider( const touchedInputErrors = Object.fromEntries(Object.entries(validateErrors).filter(([inputID]) => touchedInputs.current[inputID])); - if (!lodashIsEqual(errors, touchedInputErrors)) { + if (!deepEqual(errors, touchedInputErrors)) { setErrors(touchedInputErrors); } diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 6f273efb655ae..21c8907f2fe04 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -1,5 +1,5 @@ import {useFocusEffect, useIsFocused} from '@react-navigation/native'; -import lodashIsEqual from 'lodash/isEqual'; +import {deepEqual} from 'fast-equals'; import React, {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {InteractionManager, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; @@ -1113,7 +1113,7 @@ MoneyRequestConfirmationList.displayName = 'MoneyRequestConfirmationList'; export default memo( MoneyRequestConfirmationList, (prevProps, nextProps) => - lodashIsEqual(prevProps.transaction, nextProps.transaction) && + deepEqual(prevProps.transaction, nextProps.transaction) && prevProps.onSendMoney === nextProps.onSendMoney && prevProps.onConfirm === nextProps.onConfirm && prevProps.iouType === nextProps.iouType && @@ -1125,8 +1125,8 @@ export default memo( prevProps.isEditingSplitBill === nextProps.isEditingSplitBill && prevProps.iouCurrencyCode === nextProps.iouCurrencyCode && prevProps.iouMerchant === nextProps.iouMerchant && - lodashIsEqual(prevProps.selectedParticipants, nextProps.selectedParticipants) && - lodashIsEqual(prevProps.payeePersonalDetails, nextProps.payeePersonalDetails) && + deepEqual(prevProps.selectedParticipants, nextProps.selectedParticipants) && + deepEqual(prevProps.payeePersonalDetails, nextProps.payeePersonalDetails) && prevProps.isReadOnly === nextProps.isReadOnly && prevProps.bankAccountRoute === nextProps.bankAccountRoute && prevProps.policyID === nextProps.policyID && @@ -1140,6 +1140,6 @@ export default memo( prevProps.onToggleBillable === nextProps.onToggleBillable && prevProps.hasSmartScanFailed === nextProps.hasSmartScanFailed && prevProps.reportActionID === nextProps.reportActionID && - lodashIsEqual(prevProps.action, nextProps.action) && + deepEqual(prevProps.action, nextProps.action) && prevProps.shouldDisplayReceipt === nextProps.shouldDisplayReceipt, ); diff --git a/src/components/MoneyRequestConfirmationListFooter.tsx b/src/components/MoneyRequestConfirmationListFooter.tsx index 3c1350a0cb3dd..1c853c6976064 100644 --- a/src/components/MoneyRequestConfirmationListFooter.tsx +++ b/src/components/MoneyRequestConfirmationListFooter.tsx @@ -1,6 +1,6 @@ import {format} from 'date-fns'; import {Str} from 'expensify-common'; -import lodashIsEqual from 'lodash/isEqual'; +import {deepEqual} from 'fast-equals'; import React, {memo, useMemo} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; @@ -904,7 +904,7 @@ MoneyRequestConfirmationListFooter.displayName = 'MoneyRequestConfirmationListFo export default memo( MoneyRequestConfirmationListFooter, (prevProps, nextProps) => - lodashIsEqual(prevProps.action, nextProps.action) && + deepEqual(prevProps.action, nextProps.action) && prevProps.currency === nextProps.currency && prevProps.didConfirm === nextProps.didConfirm && prevProps.distance === nextProps.distance && @@ -926,21 +926,21 @@ export default memo( prevProps.isReadOnly === nextProps.isReadOnly && prevProps.isTypeInvoice === nextProps.isTypeInvoice && prevProps.onToggleBillable === nextProps.onToggleBillable && - lodashIsEqual(prevProps.policy, nextProps.policy) && - lodashIsEqual(prevProps.policyTagLists, nextProps.policyTagLists) && + deepEqual(prevProps.policy, nextProps.policy) && + deepEqual(prevProps.policyTagLists, nextProps.policyTagLists) && prevProps.rate === nextProps.rate && prevProps.receiptFilename === nextProps.receiptFilename && prevProps.receiptPath === nextProps.receiptPath && prevProps.reportActionID === nextProps.reportActionID && prevProps.reportID === nextProps.reportID && - lodashIsEqual(prevProps.selectedParticipants, nextProps.selectedParticipants) && + deepEqual(prevProps.selectedParticipants, nextProps.selectedParticipants) && prevProps.shouldDisplayFieldError === nextProps.shouldDisplayFieldError && prevProps.shouldDisplayReceipt === nextProps.shouldDisplayReceipt && prevProps.shouldShowCategories === nextProps.shouldShowCategories && prevProps.shouldShowMerchant === nextProps.shouldShowMerchant && prevProps.shouldShowSmartScanFields === nextProps.shouldShowSmartScanFields && prevProps.shouldShowTax === nextProps.shouldShowTax && - lodashIsEqual(prevProps.transaction, nextProps.transaction) && + deepEqual(prevProps.transaction, nextProps.transaction) && prevProps.transactionID === nextProps.transactionID && prevProps.unit === nextProps.unit, ); diff --git a/src/components/OptionRow.tsx b/src/components/OptionRow.tsx index 6d951a32b10ac..872fbf01f50a9 100644 --- a/src/components/OptionRow.tsx +++ b/src/components/OptionRow.tsx @@ -1,4 +1,4 @@ -import lodashIsEqual from 'lodash/isEqual'; +import {deepEqual} from 'fast-equals'; import React, {useEffect, useRef, useState} from 'react'; import type {StyleProp, TextStyle, ViewStyle} from 'react-native'; import {InteractionManager, StyleSheet, View} from 'react-native'; @@ -6,8 +6,8 @@ import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as OptionsListUtils from '@libs/OptionsListUtils'; -import * as ReportUtils from '@libs/ReportUtils'; +import {shouldOptionShowTooltip} from '@libs/OptionsListUtils'; +import {getDisplayNamesWithTooltips} from '@libs/ReportUtils'; import type {OptionData} from '@libs/ReportUtils'; import CONST from '@src/CONST'; import Button from './Button'; @@ -149,7 +149,7 @@ function OptionRow({ const firstIcon = option?.icons?.at(0); // We only create tooltips for the first 10 users or so since some reports have hundreds of users, causing performance to degrade. - const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips((option.participantsList ?? (option.accountID ? [option] : [])).slice(0, 10), shouldUseShortFormInTooltip); + const displayNamesWithTooltips = getDisplayNamesWithTooltips((option.participantsList ?? (option.accountID ? [option] : [])).slice(0, 10), shouldUseShortFormInTooltip); let subscriptColor = theme.appBG; if (optionIsFocused) { subscriptColor = focusedBackgroundColor; @@ -221,7 +221,7 @@ function OptionRow({ icons={option.icons} size={CONST.AVATAR_SIZE.DEFAULT} secondAvatarStyle={[StyleUtils.getBackgroundAndBorderStyle(hovered && !optionIsFocused ? hoveredBackgroundColor : subscriptColor)]} - shouldShowTooltip={showTitleTooltip && OptionsListUtils.shouldOptionShowTooltip(option)} + shouldShowTooltip={showTitleTooltip && shouldOptionShowTooltip(option)} /> ))} @@ -358,7 +358,7 @@ export default React.memo( prevProps.showSelectedState === nextProps.showSelectedState && prevProps.highlightSelected === nextProps.highlightSelected && prevProps.showTitleTooltip === nextProps.showTitleTooltip && - lodashIsEqual(prevProps.option.icons, nextProps.option.icons) && + deepEqual(prevProps.option.icons, nextProps.option.icons) && prevProps.optionIsFocused === nextProps.optionIsFocused && prevProps.option.text === nextProps.option.text && prevProps.option.alternateText === nextProps.option.alternateText && @@ -370,7 +370,7 @@ export default React.memo( prevProps.option.pendingAction === nextProps.option.pendingAction && prevProps.option.customIcon === nextProps.option.customIcon && prevProps.option.tabIndex === nextProps.option.tabIndex && - lodashIsEqual(prevProps.option.amountInputProps, nextProps.option.amountInputProps), + deepEqual(prevProps.option.amountInputProps, nextProps.option.amountInputProps), ); export type {OptionRowProps}; diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx index f64ade56311d8..3d3a8825bf995 100644 --- a/src/components/PopoverMenu.tsx +++ b/src/components/PopoverMenu.tsx @@ -1,5 +1,5 @@ /* eslint-disable react/jsx-props-no-spreading */ -import lodashIsEqual from 'lodash/isEqual'; +import {deepEqual} from 'fast-equals'; import type {ReactNode, RefObject} from 'react'; import React, {useCallback, useLayoutEffect, useMemo, useState} from 'react'; import {StyleSheet, View} from 'react-native'; @@ -443,13 +443,13 @@ PopoverMenu.displayName = 'PopoverMenu'; export default React.memo( PopoverMenu, (prevProps, nextProps) => - lodashIsEqual(prevProps.menuItems, nextProps.menuItems) && + deepEqual(prevProps.menuItems, nextProps.menuItems) && prevProps.isVisible === nextProps.isVisible && - lodashIsEqual(prevProps.anchorPosition, nextProps.anchorPosition) && + deepEqual(prevProps.anchorPosition, nextProps.anchorPosition) && prevProps.anchorRef === nextProps.anchorRef && prevProps.headerText === nextProps.headerText && prevProps.fromSidebarMediumScreen === nextProps.fromSidebarMediumScreen && - lodashIsEqual(prevProps.anchorAlignment, nextProps.anchorAlignment) && + deepEqual(prevProps.anchorAlignment, nextProps.anchorAlignment) && prevProps.animationIn === nextProps.animationIn && prevProps.animationOut === nextProps.animationOut && prevProps.animationInTiming === nextProps.animationInTiming && diff --git a/src/components/PopoverWithMeasuredContent.tsx b/src/components/PopoverWithMeasuredContent.tsx index a20c7035964c6..a811d1f6f378f 100644 --- a/src/components/PopoverWithMeasuredContent.tsx +++ b/src/components/PopoverWithMeasuredContent.tsx @@ -1,4 +1,4 @@ -import isEqual from 'lodash/isEqual'; +import {circularDeepEqual, deepEqual} from 'fast-equals'; import React, {useContext, useMemo, useState} from 'react'; import type {LayoutChangeEvent} from 'react-native'; import {View} from 'react-native'; @@ -90,8 +90,8 @@ function PopoverWithMeasuredContent({ if (!prevIsVisible && isVisible && isContentMeasured && !shouldSkipRemeasurement) { // Check if anything significant changed that would require re-measurement - const hasAnchorPositionChanged = !isEqual(prevAnchorPosition, anchorPosition); - const hasWindowSizeChanged = !isEqual(prevWindowDimensions, {windowWidth, windowHeight}); + const hasAnchorPositionChanged = !deepEqual(prevAnchorPosition, anchorPosition); + const hasWindowSizeChanged = !deepEqual(prevWindowDimensions, {windowWidth, windowHeight}); const hasStaticDimensions = popoverDimensions.width > 0 && popoverDimensions.height > 0; // Only reset if: @@ -238,7 +238,7 @@ export default React.memo(PopoverWithMeasuredContent, (prevProps, nextProps) => if (prevProps.isVisible === nextProps.isVisible && nextProps.isVisible === false) { return true; } - return isEqual(prevProps, nextProps); + return circularDeepEqual(prevProps, nextProps); }); export type {PopoverWithMeasuredContentProps}; diff --git a/src/components/Search/SearchPageHeader/SearchPageHeaderInput.tsx b/src/components/Search/SearchPageHeader/SearchPageHeaderInput.tsx index d4bd9fbb2c4fa..c35716498302b 100644 --- a/src/components/Search/SearchPageHeader/SearchPageHeaderInput.tsx +++ b/src/components/Search/SearchPageHeader/SearchPageHeaderInput.tsx @@ -1,6 +1,6 @@ import {useIsFocused} from '@react-navigation/native'; +import {deepEqual} from 'fast-equals'; import isEmpty from 'lodash/isEmpty'; -import isEqual from 'lodash/isEqual'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; @@ -155,7 +155,7 @@ function SearchPageHeaderInput({queryJSON, searchRouterListVisible, hideSearchRo setAutocompleteQueryValue(updatedUserQuery); const updatedSubstitutionsMap = getUpdatedSubstitutionsMap(singleLineUserQuery, autocompleteSubstitutions); - if (!isEqual(autocompleteSubstitutions, updatedSubstitutionsMap) && !isEmpty(updatedSubstitutionsMap)) { + if (!deepEqual(autocompleteSubstitutions, updatedSubstitutionsMap) && !isEmpty(updatedSubstitutionsMap)) { setAutocompleteSubstitutions(updatedSubstitutionsMap); } diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index 9db040127cd87..543c7e3f8045f 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -1,5 +1,5 @@ import {findFocusedRoute, useNavigationState} from '@react-navigation/native'; -import isEqual from 'lodash/isEqual'; +import {deepEqual} from 'fast-equals'; import React, {forwardRef, useCallback, useEffect, useRef, useState} from 'react'; import type {TextInputProps} from 'react-native'; import {InteractionManager, Keyboard, View} from 'react-native'; @@ -198,7 +198,7 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla setAutocompleteQueryValue(updatedUserQuery); const updatedSubstitutionsMap = getUpdatedSubstitutionsMap(singleLineUserQuery, autocompleteSubstitutions); - if (!isEqual(autocompleteSubstitutions, updatedSubstitutionsMap)) { + if (!deepEqual(autocompleteSubstitutions, updatedSubstitutionsMap)) { setAutocompleteSubstitutions(updatedSubstitutionsMap); } diff --git a/src/hooks/useDeepCompareRef.ts b/src/hooks/useDeepCompareRef.ts index c06978ce03a6e..46318ab886752 100644 --- a/src/hooks/useDeepCompareRef.ts +++ b/src/hooks/useDeepCompareRef.ts @@ -1,4 +1,4 @@ -import isEqual from 'lodash/isEqual'; +import {deepEqual} from 'fast-equals'; import {useRef} from 'react'; /** @@ -18,7 +18,7 @@ import {useRef} from 'react'; export default function useDeepCompareRef(value: T): T | undefined { const ref = useRef(undefined); // eslint-disable-next-line react-compiler/react-compiler - if (!isEqual(value, ref.current)) { + if (!deepEqual(value, ref.current)) { // eslint-disable-next-line react-compiler/react-compiler ref.current = value; } diff --git a/src/hooks/useFetchRoute.ts b/src/hooks/useFetchRoute.ts index 3232b7e02e37d..e4d2d7a42fdeb 100644 --- a/src/hooks/useFetchRoute.ts +++ b/src/hooks/useFetchRoute.ts @@ -1,8 +1,8 @@ -import isEqual from 'lodash/isEqual'; +import {deepEqual} from 'fast-equals'; import {useEffect} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; -import * as TransactionUtils from '@libs/TransactionUtils'; -import * as TransactionAction from '@userActions/Transaction'; +import {getRoute} from '@libs/actions/Transaction'; +import {getValidWaypoints, hasRoute as hasRouteTransactionUtils, isDistanceRequest as isDistanceRequestTransactionUtils} from '@libs/TransactionUtils'; import type {IOUAction} from '@src/CONST'; import CONST from '@src/CONST'; import type {Transaction} from '@src/types/onyx'; @@ -19,13 +19,13 @@ export default function useFetchRoute( ) { const {isOffline} = useNetwork(); const hasRouteError = !!transaction?.errorFields?.route; - const hasRoute = TransactionUtils.hasRoute(transaction); + const hasRoute = hasRouteTransactionUtils(transaction); const isRouteAbsentWithoutErrors = !hasRoute && !hasRouteError; const isLoadingRoute = transaction?.comment?.isLoading ?? false; - const validatedWaypoints = TransactionUtils.getValidWaypoints(waypoints); + const validatedWaypoints = getValidWaypoints(waypoints); const previousValidatedWaypoints = usePrevious(validatedWaypoints); - const haveValidatedWaypointsChanged = !isEqual(previousValidatedWaypoints, validatedWaypoints); - const isDistanceRequest = TransactionUtils.isDistanceRequest(transaction); + const haveValidatedWaypointsChanged = !deepEqual(previousValidatedWaypoints, validatedWaypoints); + const isDistanceRequest = isDistanceRequestTransactionUtils(transaction); const shouldFetchRoute = isDistanceRequest && (isRouteAbsentWithoutErrors || haveValidatedWaypointsChanged) && !isLoadingRoute && Object.keys(validatedWaypoints).length > 1; useEffect(() => { @@ -33,7 +33,7 @@ export default function useFetchRoute( return; } - TransactionAction.getRoute(transaction.transactionID, validatedWaypoints, transactionState); + getRoute(transaction.transactionID, validatedWaypoints, transactionState); }, [shouldFetchRoute, transaction?.transactionID, validatedWaypoints, isOffline, action, transactionState]); return {shouldFetchRoute, validatedWaypoints}; diff --git a/src/hooks/useSearchHighlightAndScroll.ts b/src/hooks/useSearchHighlightAndScroll.ts index be20d3af461cc..2be34918a5f1e 100644 --- a/src/hooks/useSearchHighlightAndScroll.ts +++ b/src/hooks/useSearchHighlightAndScroll.ts @@ -1,5 +1,5 @@ import {useIsFocused} from '@react-navigation/native'; -import isEqual from 'lodash/isEqual'; +import {deepEqual} from 'fast-equals'; import {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {SearchQueryJSON} from '@components/Search/types'; @@ -61,8 +61,8 @@ function useSearchHighlightAndScroll({searchResults, transactions, previousTrans return; } - const hasTransactionsIDsChange = !isEqual(transactionsIDs, previousTransactionsIDs); - const hasReportActionsIDsChange = !isEqual(reportActionsIDs, previousReportActionsIDs); + const hasTransactionsIDsChange = !deepEqual(transactionsIDs, previousTransactionsIDs); + const hasReportActionsIDsChange = !deepEqual(reportActionsIDs, previousReportActionsIDs); // Check if there is a change in the transactions or report actions list if ((!isChat && hasTransactionsIDsChange) || hasReportActionsIDsChange) { diff --git a/src/libs/Performance.tsx b/src/libs/Performance.tsx index 868b70b9673e6..74a4fe1a863ee 100644 --- a/src/libs/Performance.tsx +++ b/src/libs/Performance.tsx @@ -1,4 +1,4 @@ -import isEqual from 'lodash/isEqual'; +import {deepEqual} from 'fast-equals'; import isObject from 'lodash/isObject'; import lodashTransform from 'lodash/transform'; import React, {forwardRef, Profiler} from 'react'; @@ -17,7 +17,7 @@ import canCapturePerformanceMetrics from './Metrics'; function diffObject(object: Record, base: Record): Record { function changes(obj: Record, comparisonObject: Record): Record { return lodashTransform(obj, (result, value, key) => { - if (isEqual(value, comparisonObject[key])) { + if (deepEqual(value, comparisonObject[key])) { return; } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index d9c333fc131cc..8839bb428d695 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1,10 +1,10 @@ import {findFocusedRoute} from '@react-navigation/native'; import {format} from 'date-fns'; import {Str} from 'expensify-common'; +import {deepEqual} from 'fast-equals'; import lodashEscape from 'lodash/escape'; import lodashIntersection from 'lodash/intersection'; import isEmpty from 'lodash/isEmpty'; -import lodashIsEqual from 'lodash/isEqual'; import isNumber from 'lodash/isNumber'; import mapValues from 'lodash/mapValues'; import lodashMaxBy from 'lodash/maxBy'; @@ -8042,7 +8042,7 @@ function getChatByParticipants( const sortedParticipantsAccountIDs = participantAccountIDs.map(Number).sort(); // Only return the chat if it has all the participants - return lodashIsEqual(sortedNewParticipantList, sortedParticipantsAccountIDs); + return deepEqual(sortedNewParticipantList, sortedParticipantsAccountIDs); }); } diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 46aec3b3508d3..baaa43e19c79d 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -1,7 +1,7 @@ import {format, isValid, parse} from 'date-fns'; +import {deepEqual} from 'fast-equals'; import lodashDeepClone from 'lodash/cloneDeep'; import lodashHas from 'lodash/has'; -import lodashIsEqual from 'lodash/isEqual'; import lodashSet from 'lodash/set'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; @@ -1382,7 +1382,7 @@ function compareDuplicateTransactionFields( // Helper function to check if all comments are equal function areAllCommentsEqual(items: Array>, firstTransaction: OnyxEntry) { - return items.every((item) => lodashIsEqual(getDescription(item), getDescription(firstTransaction))); + return items.every((item) => deepEqual(getDescription(item), getDescription(firstTransaction))); } // Helper function to check if all fields are equal for a given key diff --git a/src/libs/actions/PersistedRequests.ts b/src/libs/actions/PersistedRequests.ts index ad2e3285ba7cd..2ed61cf7318a2 100644 --- a/src/libs/actions/PersistedRequests.ts +++ b/src/libs/actions/PersistedRequests.ts @@ -1,4 +1,4 @@ -import isEqual from 'lodash/isEqual'; +import {deepEqual} from 'fast-equals'; import Onyx from 'react-native-onyx'; import Log from '@libs/Log'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -18,7 +18,7 @@ Onyx.connect({ // We try to remove the next request from the persistedRequests if it is the same as ongoingRequest // so we don't process it twice. - if (isEqual(nextRequestToProcess, ongoingRequest)) { + if (deepEqual(nextRequestToProcess, ongoingRequest)) { persistedRequests = persistedRequests.slice(1); } } @@ -61,7 +61,7 @@ function endRequestAndRemoveFromQueue(requestToRemove: Request) { * If we were to remove all matching requests, we can end up with a final state that is different than what the user intended. */ const requests = [...persistedRequests]; - const index = requests.findIndex((persistedRequest) => isEqual(persistedRequest, requestToRemove)); + const index = requests.findIndex((persistedRequest) => deepEqual(persistedRequest, requestToRemove)); if (index !== -1) { requests.splice(index, 1); diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index 98008bea7d2bf..b8607f677c176 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -1,7 +1,7 @@ import {getUnixTime} from 'date-fns'; +import {deepEqual} from 'fast-equals'; import lodashClone from 'lodash/clone'; import lodashHas from 'lodash/has'; -import isEqual from 'lodash/isEqual'; import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; @@ -133,7 +133,7 @@ function saveWaypoint(transactionID: string, index: string, waypoint: RecentWayp // If current location is used, we would want to avoid saving it as a recent waypoint. This prevents the 'Your Location' // text from showing up in the address search suggestions - if (isEqual(waypoint?.address, CONST.YOUR_LOCATION_TEXT)) { + if (deepEqual(waypoint?.address, CONST.YOUR_LOCATION_TEXT)) { return; } const recentWaypointAlreadyExists = recentWaypoints.find((recentWaypoint) => recentWaypoint?.address === waypoint?.address); diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index c0ded2f99766a..e7d66ba216ac6 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -1,6 +1,6 @@ import {PortalHost} from '@gorhom/portal'; import {useIsFocused} from '@react-navigation/native'; -import lodashIsEqual from 'lodash/isEqual'; +import {deepEqual} from 'fast-equals'; import React, {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import type {FlatList, ViewStyle} from 'react-native'; import {DeviceEventEmitter, InteractionManager, View} from 'react-native'; @@ -865,4 +865,4 @@ function ReportScreen({route, navigation}: ReportScreenProps) { } ReportScreen.displayName = 'ReportScreen'; -export default memo(ReportScreen, (prevProps, nextProps) => lodashIsEqual(prevProps.route, nextProps.route)); +export default memo(ReportScreen, (prevProps, nextProps) => deepEqual(prevProps.route, nextProps.route)); diff --git a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx index 9e9448625364b..6713eaa615f28 100755 --- a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx @@ -1,4 +1,4 @@ -import lodashIsEqual from 'lodash/isEqual'; +import {deepEqual} from 'fast-equals'; import type {MutableRefObject, RefObject} from 'react'; import React, {memo, useContext, useMemo, useRef, useState} from 'react'; import {InteractionManager, View} from 'react-native'; @@ -395,6 +395,6 @@ function BaseReportActionContextMenu({ ); } -export default memo(BaseReportActionContextMenu, lodashIsEqual); +export default memo(BaseReportActionContextMenu, deepEqual); export type {BaseReportActionContextMenuProps}; diff --git a/src/pages/home/report/PureReportActionItem.tsx b/src/pages/home/report/PureReportActionItem.tsx index ca8d70b508d81..2045965dda56b 100644 --- a/src/pages/home/report/PureReportActionItem.tsx +++ b/src/pages/home/report/PureReportActionItem.tsx @@ -1,4 +1,4 @@ -import lodashIsEqual from 'lodash/isEqual'; +import {deepEqual} from 'fast-equals'; import mapValues from 'lodash/mapValues'; import React, {memo, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; import type {GestureResponderEvent, TextInput} from 'react-native'; @@ -535,7 +535,7 @@ function PureReportActionItem({ } const urls = extractLinksFromMessageHtml(action); - if (lodashIsEqual(downloadedPreviews.current, urls) || action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { + if (deepEqual(downloadedPreviews.current, urls) || action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { return; } @@ -1573,10 +1573,10 @@ export default memo(PureReportActionItem, (prevProps, nextProps) => { prevProps.displayAsGroup === nextProps.displayAsGroup && prevProps.isMostRecentIOUReportAction === nextProps.isMostRecentIOUReportAction && prevProps.shouldDisplayNewMarker === nextProps.shouldDisplayNewMarker && - lodashIsEqual(prevProps.action, nextProps.action) && - lodashIsEqual(prevProps.report?.pendingFields, nextProps.report?.pendingFields) && - lodashIsEqual(prevProps.report?.isDeletedParentAction, nextProps.report?.isDeletedParentAction) && - lodashIsEqual(prevProps.report?.errorFields, nextProps.report?.errorFields) && + deepEqual(prevProps.action, nextProps.action) && + deepEqual(prevProps.report?.pendingFields, nextProps.report?.pendingFields) && + deepEqual(prevProps.report?.isDeletedParentAction, nextProps.report?.isDeletedParentAction) && + deepEqual(prevProps.report?.errorFields, nextProps.report?.errorFields) && prevProps.report?.statusNum === nextProps.report?.statusNum && prevProps.report?.stateNum === nextProps.report?.stateNum && prevProps.report?.parentReportID === nextProps.report?.parentReportID && @@ -1593,24 +1593,24 @@ export default memo(PureReportActionItem, (prevProps, nextProps) => { prevProps.report?.nonReimbursableTotal === nextProps.report?.nonReimbursableTotal && prevProps.report?.policyAvatar === nextProps.report?.policyAvatar && prevProps.linkedReportActionID === nextProps.linkedReportActionID && - lodashIsEqual(prevProps.report?.fieldList, nextProps.report?.fieldList) && - lodashIsEqual(prevProps.transactionThreadReport, nextProps.transactionThreadReport) && - lodashIsEqual(prevProps.reportActions, nextProps.reportActions) && - lodashIsEqual(prevParentReportAction, nextParentReportAction) && + deepEqual(prevProps.report?.fieldList, nextProps.report?.fieldList) && + deepEqual(prevProps.transactionThreadReport, nextProps.transactionThreadReport) && + deepEqual(prevProps.reportActions, nextProps.reportActions) && + deepEqual(prevParentReportAction, nextParentReportAction) && prevProps.draftMessage === nextProps.draftMessage && prevProps.iouReport?.reportID === nextProps.iouReport?.reportID && - lodashIsEqual(prevProps.emojiReactions, nextProps.emojiReactions) && - lodashIsEqual(prevProps.linkedTransactionRouteError, nextProps.linkedTransactionRouteError) && - lodashIsEqual(prevProps.reportNameValuePairs, nextProps.reportNameValuePairs) && + deepEqual(prevProps.emojiReactions, nextProps.emojiReactions) && + deepEqual(prevProps.linkedTransactionRouteError, nextProps.linkedTransactionRouteError) && + deepEqual(prevProps.reportNameValuePairs, nextProps.reportNameValuePairs) && prevProps.isUserValidated === nextProps.isUserValidated && prevProps.parentReport?.reportID === nextProps.parentReport?.reportID && - lodashIsEqual(prevProps.personalDetails, nextProps.personalDetails) && - lodashIsEqual(prevProps.blockedFromConcierge, nextProps.blockedFromConcierge) && + deepEqual(prevProps.personalDetails, nextProps.personalDetails) && + deepEqual(prevProps.blockedFromConcierge, nextProps.blockedFromConcierge) && prevProps.originalReportID === nextProps.originalReportID && prevProps.isArchivedRoom === nextProps.isArchivedRoom && prevProps.isChronosReport === nextProps.isChronosReport && prevProps.isClosedExpenseReportWithNoExpenses === nextProps.isClosedExpenseReportWithNoExpenses && - lodashIsEqual(prevProps.missingPaymentMethod, nextProps.missingPaymentMethod) && + deepEqual(prevProps.missingPaymentMethod, nextProps.missingPaymentMethod) && prevProps.reimbursementDeQueuedOrCanceledActionMessage === nextProps.reimbursementDeQueuedOrCanceledActionMessage && prevProps.modifiedExpenseMessage === nextProps.modifiedExpenseMessage && prevProps.userBillingFundID === nextProps.userBillingFundID diff --git a/src/pages/home/report/ReportActionItemContentCreated.tsx b/src/pages/home/report/ReportActionItemContentCreated.tsx index 9604f913857f6..83d327fc6eb97 100644 --- a/src/pages/home/report/ReportActionItemContentCreated.tsx +++ b/src/pages/home/report/ReportActionItemContentCreated.tsx @@ -1,4 +1,4 @@ -import lodashIsEqual from 'lodash/isEqual'; +import {deepEqual} from 'fast-equals'; import React, {memo, useMemo} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; @@ -200,8 +200,8 @@ ReportActionItemContentCreated.displayName = 'ReportActionItemContentCreated'; export default memo( ReportActionItemContentCreated, (prevProps, nextProps) => - lodashIsEqual(prevProps.contextValue, nextProps.contextValue) && - lodashIsEqual(prevProps.parentReportAction, nextProps.parentReportAction) && + deepEqual(prevProps.contextValue, nextProps.contextValue) && + deepEqual(prevProps.parentReportAction, nextProps.parentReportAction) && prevProps.transactionID === nextProps.transactionID && prevProps.draftMessage === nextProps.draftMessage && prevProps.shouldHideThreadDividerLine === nextProps.shouldHideThreadDividerLine, diff --git a/src/pages/home/report/ReportFooter.tsx b/src/pages/home/report/ReportFooter.tsx index 246cdbcd51723..03fcea1e72e61 100644 --- a/src/pages/home/report/ReportFooter.tsx +++ b/src/pages/home/report/ReportFooter.tsx @@ -1,5 +1,5 @@ import {Str} from 'expensify-common'; -import lodashIsEqual from 'lodash/isEqual'; +import {deepEqual} from 'fast-equals'; import React, {memo, useCallback, useEffect, useState} from 'react'; import {Keyboard, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; @@ -238,12 +238,12 @@ ReportFooter.displayName = 'ReportFooter'; export default memo( ReportFooter, (prevProps, nextProps) => - lodashIsEqual(prevProps.report, nextProps.report) && + deepEqual(prevProps.report, nextProps.report) && prevProps.pendingAction === nextProps.pendingAction && prevProps.isComposerFullSize === nextProps.isComposerFullSize && prevProps.lastReportAction === nextProps.lastReportAction && prevProps.isReportReadyForDisplay === nextProps.isReportReadyForDisplay && - lodashIsEqual(prevProps.reportMetadata, nextProps.reportMetadata) && - lodashIsEqual(prevProps.policy?.employeeList, nextProps.policy?.employeeList) && - lodashIsEqual(prevProps.policy?.role, nextProps.policy?.role), + deepEqual(prevProps.reportMetadata, nextProps.reportMetadata) && + deepEqual(prevProps.policy?.employeeList, nextProps.policy?.employeeList) && + deepEqual(prevProps.policy?.role, nextProps.policy?.role), ); diff --git a/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx b/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx index a34d4db3061e5..a5b1334bd54c9 100644 --- a/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx +++ b/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx @@ -1,4 +1,4 @@ -import lodashIsEqual from 'lodash/isEqual'; +import {deepEqual} from 'fast-equals'; import lodashReject from 'lodash/reject'; import React, {memo, useCallback, useEffect, useMemo} from 'react'; import type {GestureResponderEvent} from 'react-native'; @@ -314,4 +314,4 @@ function MoneyRequestAttendeeSelector({attendees = [], onFinish, onAttendeesAdde MoneyRequestAttendeeSelector.displayName = 'MoneyRequestAttendeeSelector'; -export default memo(MoneyRequestAttendeeSelector, (prevProps, nextProps) => lodashIsEqual(prevProps.attendees, nextProps.attendees) && prevProps.iouType === nextProps.iouType); +export default memo(MoneyRequestAttendeeSelector, (prevProps, nextProps) => deepEqual(prevProps.attendees, nextProps.attendees) && prevProps.iouType === nextProps.iouType); diff --git a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx index acc84239a9081..7a399e2e09697 100644 --- a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx +++ b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx @@ -1,4 +1,4 @@ -import lodashIsEqual from 'lodash/isEqual'; +import {deepEqual} from 'fast-equals'; import lodashPick from 'lodash/pick'; import lodashReject from 'lodash/reject'; import React, {memo, useCallback, useEffect, useMemo, useState} from 'react'; @@ -624,4 +624,4 @@ function MoneyRequestParticipantsSelector({ MoneyRequestParticipantsSelector.displayName = 'MoneyTemporaryForRefactorRequestParticipantsSelector'; -export default memo(MoneyRequestParticipantsSelector, (prevProps, nextProps) => lodashIsEqual(prevProps.participants, nextProps.participants) && prevProps.iouType === nextProps.iouType); +export default memo(MoneyRequestParticipantsSelector, (prevProps, nextProps) => deepEqual(prevProps.participants, nextProps.participants) && prevProps.iouType === nextProps.iouType); diff --git a/src/pages/iou/request/step/IOURequestStepAttendees.tsx b/src/pages/iou/request/step/IOURequestStepAttendees.tsx index 6e9f674d7a790..abc1bdf95e068 100644 --- a/src/pages/iou/request/step/IOURequestStepAttendees.tsx +++ b/src/pages/iou/request/step/IOURequestStepAttendees.tsx @@ -1,4 +1,4 @@ -import lodashIsEqual from 'lodash/isEqual'; +import {deepEqual} from 'fast-equals'; import React, {useCallback, useState} from 'react'; import {useOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; @@ -40,7 +40,8 @@ function IOURequestStepAttendees({ policyCategories, }: IOURequestStepAttendeesProps) { const isEditing = action === CONST.IOU.ACTION.EDIT; - const [transaction] = useOnyx(`${isEditing ? ONYXKEYS.COLLECTION.TRANSACTION : ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID || CONST.DEFAULT_NUMBER_ID}`); + // eslint-disable-next-line rulesdir/no-default-id-values + const [transaction] = useOnyx(`${isEditing ? ONYXKEYS.COLLECTION.TRANSACTION : ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID || CONST.DEFAULT_NUMBER_ID}`, {canBeMissing: true}); const [attendees, setAttendees] = useState(() => getAttendees(transaction)); const previousAttendees = usePrevious(attendees); const {translate} = useLocalize(); @@ -50,7 +51,7 @@ function IOURequestStepAttendees({ if (attendees.length <= 0) { return; } - if (!lodashIsEqual(previousAttendees, attendees)) { + if (!deepEqual(previousAttendees, attendees)) { setMoneyRequestAttendees(transactionID, attendees, !isEditing); if (isEditing) { updateMoneyRequestAttendees(transactionID, reportID, attendees, policy, policyTags, policyCategories, transactionViolations ?? undefined); diff --git a/src/pages/iou/request/step/IOURequestStepDistance.tsx b/src/pages/iou/request/step/IOURequestStepDistance.tsx index 88a8a53df1cff..6338d79ff1979 100644 --- a/src/pages/iou/request/step/IOURequestStepDistance.tsx +++ b/src/pages/iou/request/step/IOURequestStepDistance.tsx @@ -1,5 +1,5 @@ +import {deepEqual} from 'fast-equals'; import isEmpty from 'lodash/isEmpty'; -import isEqual from 'lodash/isEqual'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; // eslint-disable-next-line no-restricted-imports @@ -428,7 +428,7 @@ function IOURequestStepDistance({ const updateWaypoints = useCallback( ({data}: DataParams) => { - if (isEqual(waypointsList, data)) { + if (deepEqual(waypointsList, data)) { return; } @@ -468,8 +468,8 @@ function IOURequestStepDistance({ const oldWaypoints = transactionBackup?.comment?.waypoints ?? {}; const oldAddresses = Object.fromEntries(Object.entries(oldWaypoints).map(([key, waypoint]) => [key, 'address' in waypoint ? waypoint.address : {}])); const addresses = Object.fromEntries(Object.entries(waypoints).map(([key, waypoint]) => [key, 'address' in waypoint ? waypoint.address : {}])); - const hasRouteChanged = !isEqual(transactionBackup?.routes, transaction?.routes); - if (isEqual(oldAddresses, addresses)) { + const hasRouteChanged = !deepEqual(transactionBackup?.routes, transaction?.routes); + if (deepEqual(oldAddresses, addresses)) { navigateBack(); return; } diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index 7776871a32e01..dec22ddb5ba4b 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -1,5 +1,5 @@ import {useIsFocused} from '@react-navigation/native'; -import lodashIsEqual from 'lodash/isEqual'; +import {deepEqual} from 'fast-equals'; import React, {useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; import type {TextInput} from 'react-native'; import {InteractionManager, View} from 'react-native'; @@ -214,7 +214,7 @@ function WorkspaceMembersPage({personalDetails, route, policy}: WorkspaceMembers }, [validateSelection]); useEffect(() => { - if (removeMembersConfirmModalVisible && !lodashIsEqual(accountIDs, prevAccountIDs)) { + if (removeMembersConfirmModalVisible && !deepEqual(accountIDs, prevAccountIDs)) { setRemoveMembersConfirmModalVisible(false); } setSelectedEmployees((prevSelectedEmployees) => { diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 737bf745bfe1e..0cadd773a0335 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -1,6 +1,6 @@ import {renderHook} from '@testing-library/react-native'; import {format} from 'date-fns'; -import isEqual from 'lodash/isEqual'; +import {deepEqual} from 'fast-equals'; import type {OnyxCollection, OnyxEntry, OnyxInputValue} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import useReportWithTransactionsAndViolations from '@hooks/useReportWithTransactionsAndViolations'; @@ -1834,7 +1834,8 @@ describe('actions/IOU', () => { // 5. The chat report with Rory + Vit (new) vitChatReport = Object.values(allReports ?? {}).find( (report) => - report?.type === CONST.REPORT.TYPE.CHAT && isEqual(report.participants, {[RORY_ACCOUNT_ID]: RORY_PARTICIPANT, [VIT_ACCOUNT_ID]: VIT_PARTICIPANT}), + report?.type === CONST.REPORT.TYPE.CHAT && + deepEqual(report.participants, {[RORY_ACCOUNT_ID]: RORY_PARTICIPANT, [VIT_ACCOUNT_ID]: VIT_PARTICIPANT}), ); expect(isEmptyObject(vitChatReport)).toBe(false); expect(vitChatReport?.pendingFields).toStrictEqual({createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}); @@ -1848,7 +1849,7 @@ describe('actions/IOU', () => { groupChat = Object.values(allReports ?? {}).find( (report) => report?.type === CONST.REPORT.TYPE.CHAT && - isEqual(report.participants, { + deepEqual(report.participants, { [CARLOS_ACCOUNT_ID]: CARLOS_PARTICIPANT, [JULES_ACCOUNT_ID]: JULES_PARTICIPANT, [VIT_ACCOUNT_ID]: VIT_PARTICIPANT,