Skip to content

Commit

Permalink
Merge pull request #9523 from marmelab/reference-many-field-set-filte…
Browse files Browse the repository at this point in the history
…rs-debounce

Fix `useReferenceManyFieldController` does not debounce `setFilters`
  • Loading branch information
fzaninotto authored Dec 15, 2023
2 parents ce4ab9e + 979f3dc commit e5d562e
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 6 deletions.
16 changes: 16 additions & 0 deletions docs/ReferenceManyField.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ This example leverages [`<SingleFieldList>`](./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 |

`<ReferenceManyField>` also accepts the [common field props](./Fields.md#common-field-props), except `emptyText` (use the child `empty` prop instead).

Expand Down Expand Up @@ -172,6 +173,21 @@ export const AuthorShow = () => (
);
```

## `debounce`

By default, `<ReferenceManyField>` 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 `<ReferenceManyField>` component:

```jsx
// wait 1 seconds instead of 500 milliseconds before calling the dataProvider
const PostCommentsField = () => (
<ReferenceManyField debounce={1000}>
...
</ReferenceManyField>
);
```

## `filter`

You can filter the query used to populate the possible values. Use the `filter` prop for that.
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 <div>isLoading: {isLoading.toString()}</div>;
return <div>isLoading: {isLoading?.toString()}</div>;
};
const dataProvider = testDataProvider({
getManyReference: () => Promise.resolve({ data: [], total: 0 }),
Expand Down Expand Up @@ -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 <div>isLoading: {isLoading.toString()}</div>;
return <div>isLoading: {isLoading?.toString()}</div>;
};
const dataProvider = testDataProvider({
getManyReference: () =>
Expand Down Expand Up @@ -273,4 +273,50 @@ 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 }) => (
<input
aria-label="search"
type="text"
value={filterValues.q || ''}
onChange={event => {
setFilters({ q: event.target.value });
}}
/>
);
const dataProvider = testDataProvider();
const getManyReference = jest.spyOn(dataProvider, 'getManyReference');
render(
<CoreAdminContext dataProvider={dataProvider}>
<ReferenceManyFieldController
resource="authors"
source="id"
record={{ id: 123, name: 'James Joyce' }}
reference="books"
target="author_id"
>
{childFunction}
</ReferenceManyFieldController>
</CoreAdminContext>
);
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,
});
});
});
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -15,6 +16,7 @@ import { useResourceContext } from '../../core';
export interface UseReferenceManyFieldControllerParams<
RecordType extends RaRecord = RaRecord
> {
debounce?: number;
filter?: any;
page?: number;
perPage?: number;
Expand Down Expand Up @@ -61,6 +63,7 @@ export const useReferenceManyFieldController = <
props: UseReferenceManyFieldControllerParams<RecordType>
): ListControllerResult<ReferenceRecordType> => {
const {
debounce = 500,
reference,
record,
target,
Expand Down Expand Up @@ -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)) {
Expand Down
3 changes: 3 additions & 0 deletions packages/ra-ui-materialui/src/field/ReferenceManyField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export const ReferenceManyField = <
) => {
const {
children,
debounce,
filter = defaultFilter,
page = 1,
pagination = null,
Expand All @@ -83,6 +84,7 @@ export const ReferenceManyField = <
RecordType,
ReferenceRecordType
>({
debounce,
filter,
page,
perPage,
Expand All @@ -108,6 +110,7 @@ export interface ReferenceManyFieldProps<
RecordType extends Record<string, any> = Record<string, any>
> extends FieldProps<RecordType> {
children: ReactNode;
debounce?: number;
filter?: FilterPayload;
page?: number;
pagination?: ReactElement;
Expand Down

0 comments on commit e5d562e

Please sign in to comment.