From edc103561fb486f15ad30a3f4ab55a11174a8485 Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Wed, 13 Dec 2023 14:56:08 +0100
Subject: [PATCH 1/4] Fix `useReferenceManyFieldController` does not debounce
`setFilters`
---
.../useReferenceManyFieldController.spec.tsx | 53 +++++++++++++++++--
.../field/useReferenceManyFieldController.ts | 24 +++++++--
2 files changed, 71 insertions(+), 6 deletions(-)
diff --git a/packages/ra-core/src/controller/field/useReferenceManyFieldController.spec.tsx b/packages/ra-core/src/controller/field/useReferenceManyFieldController.spec.tsx
index 3c12d36ab0e..e7719305864 100644
--- a/packages/ra-core/src/controller/field/useReferenceManyFieldController.spec.tsx
+++ b/packages/ra-core/src/controller/field/useReferenceManyFieldController.spec.tsx
@@ -1,5 +1,5 @@
import * as React from 'react';
-import { render, screen, waitFor } from '@testing-library/react';
+import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import expect from 'expect';
import { testDataProvider } from '../../dataProvider/testDataProvider';
@@ -19,7 +19,7 @@ const ReferenceManyFieldController = props => {
describe('useReferenceManyFieldController', () => {
it('should set isLoading to true when related records are not yet fetched', async () => {
const ComponentToTest = ({ isLoading }: { isLoading?: boolean }) => {
- return
isLoading: {isLoading.toString()}
;
+ return isLoading: {isLoading?.toString()}
;
};
const dataProvider = testDataProvider({
getManyReference: () => Promise.resolve({ data: [], total: 0 }),
@@ -47,7 +47,7 @@ describe('useReferenceManyFieldController', () => {
it('should set isLoading to false when related records have been fetched and there are results', async () => {
const ComponentToTest = ({ isLoading }: { isLoading?: boolean }) => {
- return isLoading: {isLoading.toString()}
;
+ return isLoading: {isLoading?.toString()}
;
};
const dataProvider = testDataProvider({
getManyReference: () =>
@@ -273,4 +273,51 @@ describe('useReferenceManyFieldController', () => {
);
});
});
+
+ it('should take only last change in case of a burst of setFilters calls (case of inputs being currently edited)', async () => {
+ let childFunction = ({ setFilters, filterValues }) => (
+ // TODO: we shouldn't import mui components in ra-core
+ {
+ setFilters({ q: event.target.value });
+ }}
+ />
+ );
+ const dataProvider = testDataProvider();
+ const getManyReference = jest.spyOn(dataProvider, 'getManyReference');
+ render(
+
+
+ {childFunction}
+
+
+ );
+ const searchInput = screen.getByLabelText('search');
+
+ fireEvent.change(searchInput, { target: { value: 'hel' } });
+ fireEvent.change(searchInput, { target: { value: 'hell' } });
+ fireEvent.change(searchInput, { target: { value: 'hello' } });
+
+ await waitFor(() => new Promise(resolve => setTimeout(resolve, 600)));
+
+ // Called twice: on load and on filter changes
+ expect(getManyReference).toHaveBeenCalledTimes(2);
+ expect(getManyReference).toHaveBeenCalledWith('books', {
+ target: 'author_id',
+ id: 123,
+ filter: { q: 'hello' },
+ pagination: { page: 1, perPage: 25 },
+ sort: { field: 'id', order: 'DESC' },
+ meta: undefined,
+ });
+ });
});
diff --git a/packages/ra-core/src/controller/field/useReferenceManyFieldController.ts b/packages/ra-core/src/controller/field/useReferenceManyFieldController.ts
index 9c16fc56980..ad2978dec95 100644
--- a/packages/ra-core/src/controller/field/useReferenceManyFieldController.ts
+++ b/packages/ra-core/src/controller/field/useReferenceManyFieldController.ts
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useRef } from 'react';
import get from 'lodash/get';
import isEqual from 'lodash/isEqual';
+import lodashDebounce from 'lodash/debounce';
import { useSafeSetState, removeEmpty } from '../../util';
import { useGetManyReference } from '../../dataProvider';
@@ -15,6 +16,7 @@ import { useResourceContext } from '../../core';
export interface UseReferenceManyFieldControllerParams<
RecordType extends RaRecord = RaRecord
> {
+ debounce?: number;
filter?: any;
page?: number;
perPage?: number;
@@ -61,6 +63,7 @@ export const useReferenceManyFieldController = <
props: UseReferenceManyFieldControllerParams
): ListControllerResult => {
const {
+ debounce = 500,
reference,
record,
target,
@@ -128,14 +131,29 @@ export const useReferenceManyFieldController = <
},
[setDisplayedFilters, setFilterValues]
);
- const setFilters = useCallback(
- (filters, displayedFilters) => {
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ const debouncedSetFilters = useCallback(
+ lodashDebounce((filters, displayedFilters) => {
setFilterValues(removeEmpty(filters));
setDisplayedFilters(displayedFilters);
setPage(1);
- },
+ }, debounce),
[setDisplayedFilters, setFilterValues, setPage]
);
+
+ const setFilters = useCallback(
+ (filters, displayedFilters, debounce = true) => {
+ if (debounce) {
+ debouncedSetFilters(filters, displayedFilters);
+ } else {
+ setFilterValues(removeEmpty(filters));
+ setDisplayedFilters(displayedFilters);
+ setPage(1);
+ }
+ },
+ [setDisplayedFilters, setFilterValues, setPage, debouncedSetFilters]
+ );
// handle filter prop change
useEffect(() => {
if (!isEqual(filter, filterRef.current)) {
From e23f7b9a669feb465fd4f235b33ca6548645f69d Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Wed, 13 Dec 2023 16:28:44 +0100
Subject: [PATCH 2/4] Remove unnecessary comment
---
.../controller/field/useReferenceManyFieldController.spec.tsx | 1 -
1 file changed, 1 deletion(-)
diff --git a/packages/ra-core/src/controller/field/useReferenceManyFieldController.spec.tsx b/packages/ra-core/src/controller/field/useReferenceManyFieldController.spec.tsx
index e7719305864..dbac5cff76f 100644
--- a/packages/ra-core/src/controller/field/useReferenceManyFieldController.spec.tsx
+++ b/packages/ra-core/src/controller/field/useReferenceManyFieldController.spec.tsx
@@ -276,7 +276,6 @@ describe('useReferenceManyFieldController', () => {
it('should take only last change in case of a burst of setFilters calls (case of inputs being currently edited)', async () => {
let childFunction = ({ setFilters, filterValues }) => (
- // TODO: we shouldn't import mui components in ra-core
Date: Thu, 14 Dec 2023 10:32:59 +0100
Subject: [PATCH 3/4] Update ReferenceManyField
---
docs/ReferenceManyField.md | 16 ++++++++++++++++
.../src/field/ReferenceManyField.tsx | 3 +++
2 files changed, 19 insertions(+)
diff --git a/docs/ReferenceManyField.md b/docs/ReferenceManyField.md
index c95bb1d465a..89604ac9545 100644
--- a/docs/ReferenceManyField.md
+++ b/docs/ReferenceManyField.md
@@ -111,6 +111,7 @@ This example leverages [``](./SingleFieldList.md) to display an
| `pagination` | Optional | `Element` | - | Pagination element to display pagination controls. empty by default (no pagination) |
| `perPage` | Optional | `number` | 25 | Maximum number of referenced records to fetch |
| `sort` | Optional | `{ field, order }` | `{ field: 'id', order: 'DESC' }` | Sort order to use when fetching the related records, passed to `getManyReference()` |
+| `debounce` | Optional | `number` | 500 | debounce time in ms for the `setFilters` callbacks |
`` also accepts the [common field props](./Fields.md#common-field-props), except `emptyText` (use the child `empty` prop instead).
@@ -172,6 +173,21 @@ export const AuthorShow = () => (
);
```
+## `debounce`
+
+By default, `` does not refresh the data as soon as the user enters data in the filter form. Instead, it waits for half a second of user inactivity (via `lodash.debounce`) before calling the `dataProvider` on filter change. This is to prevent repeated (and useless) calls to the API.
+
+You can customize the debounce duration in milliseconds - or disable it completely - by passing a `debounce` prop to the `` component:
+
+```jsx
+// wait 1 seconds instead of 500 milliseconds befoce calling the dataProvider
+const PostCommentsField = () => (
+
+ ...
+
+);
+```
+
## `filter`
You can filter the query used to populate the possible values. Use the `filter` prop for that.
diff --git a/packages/ra-ui-materialui/src/field/ReferenceManyField.tsx b/packages/ra-ui-materialui/src/field/ReferenceManyField.tsx
index a02ce9336dd..8f5291e3a15 100644
--- a/packages/ra-ui-materialui/src/field/ReferenceManyField.tsx
+++ b/packages/ra-ui-materialui/src/field/ReferenceManyField.tsx
@@ -67,6 +67,7 @@ export const ReferenceManyField = <
) => {
const {
children,
+ debounce,
filter = defaultFilter,
page = 1,
pagination = null,
@@ -83,6 +84,7 @@ export const ReferenceManyField = <
RecordType,
ReferenceRecordType
>({
+ debounce,
filter,
page,
perPage,
@@ -108,6 +110,7 @@ export interface ReferenceManyFieldProps<
RecordType extends Record = Record
> extends FieldProps {
children: ReactNode;
+ debounce?: number;
filter?: FilterPayload;
page?: number;
pagination?: ReactElement;
From 979f3dcba064b9da8433de73233c6ad514c9dba8 Mon Sep 17 00:00:00 2001
From: Jean-Baptiste Kaiser
Date: Thu, 14 Dec 2023 11:07:36 +0100
Subject: [PATCH 4/4] [no ci] Update docs/ReferenceManyField.md
---
docs/ReferenceManyField.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/ReferenceManyField.md b/docs/ReferenceManyField.md
index 89604ac9545..124b38b09cf 100644
--- a/docs/ReferenceManyField.md
+++ b/docs/ReferenceManyField.md
@@ -180,7 +180,7 @@ By default, `` does not refresh the data as soon as the user
You can customize the debounce duration in milliseconds - or disable it completely - by passing a `debounce` prop to the `` component:
```jsx
-// wait 1 seconds instead of 500 milliseconds befoce calling the dataProvider
+// wait 1 seconds instead of 500 milliseconds before calling the dataProvider
const PostCommentsField = () => (
...