Eliminate unnecessary API calls when persisting field (#12429)

Fixes #10177

Modified `usePersistField` to check for deep equality between the value
to persist and the current record store value before sending an update
query.
This commit is contained in:
Raphaël Bosi
2025-06-03 12:20:57 +02:00
committed by GitHub
parent ece2784ed7
commit 70cc3e75fe
2 changed files with 87 additions and 14 deletions

View File

@ -1,9 +1,3 @@
import { gql } from '@apollo/client';
import { MockedResponse } from '@apollo/client/testing';
import { act, renderHook, waitFor } from '@testing-library/react';
import { ReactNode } from 'react';
import { useRecoilValue } from 'recoil';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { PERSON_FRAGMENT_WITH_DEPTH_ONE_RELATIONS } from '@/object-record/hooks/__mocks__/personFragments'; import { PERSON_FRAGMENT_WITH_DEPTH_ONE_RELATIONS } from '@/object-record/hooks/__mocks__/personFragments';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
@ -20,6 +14,11 @@ import { usePersistField } from '@/object-record/record-field/hooks/usePersistFi
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition'; import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { gql } from '@apollo/client';
import { MockedResponse } from '@apollo/client/testing';
import { renderHook, waitFor } from '@testing-library/react';
import { ReactNode, act } from 'react';
import { useRecoilValue } from 'recoil';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
const query = gql` const query = gql`
@ -30,6 +29,13 @@ const query = gql`
} }
`; `;
const phoneMock = {
primaryPhoneNumber: '123 456',
primaryPhoneCountryCode: 'US',
primaryPhoneCallingCode: '+1',
additionalPhones: [],
};
const mocks: MockedResponse[] = [ const mocks: MockedResponse[] = [
{ {
request: { request: {
@ -37,12 +43,7 @@ const mocks: MockedResponse[] = [
variables: { variables: {
idToUpdate: 'recordId', idToUpdate: 'recordId',
input: { input: {
phones: { phones: phoneMock,
primaryPhoneNumber: '123 456',
primaryPhoneCountryCode: 'US',
primaryPhoneCallingCode: '+1',
additionalPhones: [],
},
}, },
}, },
}, },
@ -117,11 +118,15 @@ const PhoneWrapper = getWrapper(phonesFieldDefinition);
const RelationWrapper = getWrapper(relationFieldDefinition); const RelationWrapper = getWrapper(relationFieldDefinition);
describe('usePersistField', () => { describe('usePersistField', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should work as expected', async () => { it('should work as expected', async () => {
const { result } = renderHook( const { result } = renderHook(
() => { () => {
const entityFields = useRecoilValue( const entityFields = useRecoilValue(
recordStoreFamilySelector({ recordId, fieldName: 'phone' }), recordStoreFamilySelector({ recordId, fieldName: 'phones' }),
); );
return { return {
@ -172,4 +177,55 @@ describe('usePersistField', () => {
expect(mocks[1].result).toHaveBeenCalled(); expect(mocks[1].result).toHaveBeenCalled();
}); });
}); });
it('should skip persistence value has not changed', async () => {
const { result } = renderHook(
() => {
const entityFields = useRecoilValue(
recordStoreFamilySelector({ recordId, fieldName: 'phones' }),
);
return {
persistField: usePersistField(),
entityFields,
};
},
{ wrapper: PhoneWrapper },
);
act(() => {
result.current.persistField(phoneMock);
});
await waitFor(() => {
expect(mocks[0].result).not.toHaveBeenCalled();
});
});
it('should skip persistence when relation field value has not changed', async () => {
const { result } = renderHook(
() => {
const entityFields = useRecoilValue(
recordStoreFamilySelector({
recordId,
fieldName: 'company',
}),
);
return {
persistField: usePersistField(),
entityFields,
};
},
{ wrapper: RelationWrapper },
);
act(() => {
result.current.persistField({ id: 'companyId' });
});
await waitFor(() => {
expect(mocks[1].result).not.toHaveBeenCalled();
});
});
}); });

View File

@ -33,6 +33,7 @@ import { isFieldRichTextV2 } from '@/object-record/record-field/types/guards/isF
import { isFieldRichTextValue } from '@/object-record/record-field/types/guards/isFieldRichTextValue'; import { isFieldRichTextValue } from '@/object-record/record-field/types/guards/isFieldRichTextValue';
import { isFieldRichTextV2Value } from '@/object-record/record-field/types/guards/isFieldRichTextValueV2'; import { isFieldRichTextV2Value } from '@/object-record/record-field/types/guards/isFieldRichTextValueV2';
import { getForeignKeyNameFromRelationFieldName } from '@/object-record/utils/getForeignKeyNameFromRelationFieldName'; import { getForeignKeyNameFromRelationFieldName } from '@/object-record/utils/getForeignKeyNameFromRelationFieldName';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { FieldContext } from '../contexts/FieldContext'; import { FieldContext } from '../contexts/FieldContext';
import { isFieldBoolean } from '../types/guards/isFieldBoolean'; import { isFieldBoolean } from '../types/guards/isFieldBoolean';
import { isFieldBooleanValue } from '../types/guards/isFieldBooleanValue'; import { isFieldBooleanValue } from '../types/guards/isFieldBooleanValue';
@ -57,7 +58,7 @@ export const usePersistField = () => {
const [updateRecord] = useUpdateRecord(); const [updateRecord] = useUpdateRecord();
const persistField = useRecoilCallback( const persistField = useRecoilCallback(
({ set }) => ({ set, snapshot }) =>
(valueToPersist: unknown) => { (valueToPersist: unknown) => {
const fieldIsRelationToOneObject = const fieldIsRelationToOneObject =
isFieldRelationToOneObject( isFieldRelationToOneObject(
@ -156,6 +157,22 @@ export const usePersistField = () => {
if (isValuePersistable) { if (isValuePersistable) {
const fieldName = fieldDefinition.metadata.fieldName; const fieldName = fieldDefinition.metadata.fieldName;
const currentValue: any = snapshot
.getLoadable(recordStoreFamilySelector({ recordId, fieldName }))
.getValue();
if (
fieldIsRelationToOneObject &&
valueToPersist?.id === currentValue?.id
) {
return;
}
if (isDeeplyEqual(valueToPersist, currentValue)) {
return;
}
set( set(
recordStoreFamilySelector({ recordId, fieldName }), recordStoreFamilySelector({ recordId, fieldName }),
valueToPersist, valueToPersist,