Skip to content

Commit 3870e87

Browse files
authored
Merge pull request #5849 from ValentinH/add-enabled-options-to-queries
[RFR] Add enabled options to query hooks
2 parents 541fff7 + 6207363 commit 3870e87

12 files changed

+143
-11
lines changed

docs/Actions.md

+20
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,26 @@ const BulkDeletePostsButton = ({ selectedIds }) => {
408408
};
409409
```
410410

411+
## Synchronizing Dependant Queries
412+
`useQuery` and all its corresponding specialized hooks support an `enabled` option. This is useful if you need to have a query executed only when a condition is met. For example, in the following example, we only fetch the categories if we have at least one post:
413+
```jsx
414+
// fetch posts
415+
const { ids, data: posts, loading: isLoading } = useGetList(
416+
'posts',
417+
{ page: 1, perPage: 20 },
418+
{ field: 'name', order: 'ASC' },
419+
{}
420+
);
421+
422+
// then fetch categories for these posts
423+
const { data: categories, loading: isLoadingCategories } = useGetMany(
424+
'categories',
425+
ids.map(id=> posts[id].category_id),
426+
// run only if the first query returns non-empty result
427+
{ enabled: ids.length > 0 }
428+
);
429+
```
430+
411431
## Handling Side Effects In `useDataProvider`
412432

413433
`useDataProvider` returns a `dataProvider` object. Each call to its method return a Promise, allowing adding business logic on success in `then()`, and on failure in `catch()`.

packages/ra-core/src/dataProvider/getDataProviderCallArguments.ts

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const OptionsProperties = [
99
'onSuccess',
1010
'undoable',
1111
'mutationMode',
12+
'enabled',
1213
];
1314

1415
const isDataProviderOptions = (value: any) => {

packages/ra-core/src/dataProvider/useDataProvider.spec.js

+58
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,64 @@ describe('useDataProvider', () => {
319319
expect(onFailure.mock.calls[0][0]).toEqual(new Error('foo'));
320320
});
321321

322+
it('should accept an enabled option to block the query until a condition is met', async () => {
323+
const UseGetOneWithEnabled = () => {
324+
const [data, setData] = useState();
325+
const [error, setError] = useState();
326+
const [isEnabled, setIsEnabled] = useState(false);
327+
const dataProvider = useDataProvider();
328+
useEffect(() => {
329+
dataProvider
330+
.getOne('dummy', {}, { enabled: isEnabled })
331+
.then(res => setData(res.data))
332+
.catch(e => setError(e));
333+
}, [dataProvider, isEnabled]);
334+
335+
let content = <div data-testid="loading">loading</div>;
336+
if (error)
337+
content = <div data-testid="error">{error.message}</div>;
338+
if (data)
339+
content = (
340+
<div data-testid="data">{JSON.stringify(data)}</div>
341+
);
342+
return (
343+
<div>
344+
{content}
345+
<button onClick={() => setIsEnabled(e => !e)}>
346+
toggle
347+
</button>
348+
</div>
349+
);
350+
};
351+
const getOne = jest
352+
.fn()
353+
.mockResolvedValue({ data: { id: 1, title: 'foo' } });
354+
const dataProvider = { getOne };
355+
const { queryByTestId, getByRole } = renderWithRedux(
356+
<DataProviderContext.Provider value={dataProvider}>
357+
<UseGetOneWithEnabled />
358+
</DataProviderContext.Provider>
359+
);
360+
expect(queryByTestId('loading')).not.toBeNull();
361+
await act(async () => {
362+
await new Promise(resolve => setTimeout(resolve));
363+
});
364+
expect(getOne).not.toBeCalled();
365+
expect(queryByTestId('loading')).not.toBeNull();
366+
367+
// enable the query
368+
fireEvent.click(getByRole('button', { name: 'toggle' }));
369+
370+
await act(async () => {
371+
await new Promise(resolve => setTimeout(resolve));
372+
});
373+
expect(getOne).toBeCalledTimes(1);
374+
expect(queryByTestId('loading')).toBeNull();
375+
expect(queryByTestId('data').textContent).toBe(
376+
'{"id":1,"title":"foo"}'
377+
);
378+
});
379+
322380
describe('mutationMode', () => {
323381
it('should wait for response to dispatch side effects in pessimistic mode', async () => {
324382
let resolveUpdate;

packages/ra-core/src/dataProvider/useDataProvider.ts

+8
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ const useDataProvider = (): DataProviderProxy => {
134134
onSuccess = undefined,
135135
onFailure = undefined,
136136
mutationMode = undoable ? 'undoable' : 'pessimistic',
137+
enabled = true,
137138
...rest
138139
} = options || {};
139140

@@ -157,6 +158,13 @@ const useDataProvider = (): DataProviderProxy => {
157158
'You must pass an onSuccess callback calling notify() to use the undoable mode'
158159
);
159160
}
161+
if (typeof enabled !== 'boolean') {
162+
throw new Error('The enabled option must be a boolean');
163+
}
164+
165+
if (enabled === false) {
166+
return Promise.resolve({});
167+
}
160168

161169
const params = {
162170
resource,

packages/ra-core/src/dataProvider/useGetList.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
Identifier,
99
Record,
1010
RecordMap,
11+
UseDataProviderOptions,
1112
} from '../types';
1213
import useQueryWithStore from './useQueryWithStore';
1314

@@ -31,7 +32,10 @@ const defaultData = {};
3132
* @param {Object} pagination The request pagination { page, perPage }, e.g. { page: 1, perPage: 10 }
3233
* @param {Object} sort The request sort { field, order }, e.g. { field: 'id', order: 'DESC' }
3334
* @param {Object} filter The request filters, e.g. { title: 'hello, world' }
34-
* @param {Object} options Options object to pass to the dataProvider. May include side effects to be executed upon success or failure, e.g. { onSuccess: { refresh: true } }
35+
* @param {Object} options Options object to pass to the dataProvider.
36+
* @param {boolean} options.enabled Flag to conditionally run the query. If it's false, the query will not run
37+
* @param {Function} options.onSuccess Side effect function to be executed upon success, e.g. { onSuccess: { refresh: true } }
38+
* @param {Function} options.onFailure Side effect function to be executed upon failure, e.g. { onFailure: error => notify(error.message) }
3539
*
3640
* @returns The current request state. Destructure as { data, total, ids, error, loading, loaded }.
3741
*
@@ -57,7 +61,7 @@ const useGetList = <RecordType extends Record = Record>(
5761
pagination: PaginationPayload,
5862
sort: SortPayload,
5963
filter: object,
60-
options?: any
64+
options?: UseDataProviderOptions
6165
): {
6266
data?: RecordMap<RecordType>;
6367
ids?: Identifier[];

packages/ra-core/src/dataProvider/useGetMany.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ interface Query {
2424
interface QueriesToCall {
2525
[resource: string]: Query[];
2626
}
27+
interface UseGetManyOptions {
28+
onSuccess?: Callback;
29+
onFailure?: Callback;
30+
enabled?: boolean;
31+
}
2732
interface UseGetManyResult {
2833
data: Record[];
2934
error?: any;
@@ -59,7 +64,10 @@ const DataProviderOptions = { action: CRUD_GET_MANY };
5964
*
6065
* @param resource The resource name, e.g. 'posts'
6166
* @param ids The resource identifiers, e.g. [123, 456, 789]
62-
* @param options Options object to pass to the dataProvider. May include side effects to be executed upon success or failure, e.g. { onSuccess: { refresh: true } }
67+
* @param {Object} options Options object to pass to the dataProvider.
68+
* @param {boolean} options.enabled Flag to conditionally run the query. If it's false, the query will not run
69+
* @param {Function} options.onSuccess Side effect function to be executed upon success, e.g. { onSuccess: { refresh: true } }
70+
* @param {Function} options.onFailure Side effect function to be executed upon failure, e.g. { onFailure: error => notify(error.message) }
6371
*
6472
* @returns The current request state. Destructure as { data, error, loading, loaded }.
6573
*
@@ -83,7 +91,7 @@ const DataProviderOptions = { action: CRUD_GET_MANY };
8391
const useGetMany = (
8492
resource: string,
8593
ids: Identifier[],
86-
options: any = {}
94+
options: UseGetManyOptions = {}
8795
): UseGetManyResult => {
8896
// we can't use useQueryWithStore here because we're aggregating queries first
8997
// therefore part of the useQueryWithStore logic will have to be repeated below

packages/ra-core/src/dataProvider/useGetManyReference.ts

+12-2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ import {
1818
const defaultIds = [];
1919
const defaultData = {};
2020

21+
interface UseGetManyReferenceOptions {
22+
onSuccess?: (args?: any) => void;
23+
onFailure?: (error: any) => void;
24+
enabled?: boolean;
25+
[key: string]: any;
26+
}
27+
2128
/**
2229
* Call the dataProvider.getManyReference() method and return the resolved result
2330
* as well as the loading state.
@@ -38,7 +45,10 @@ const defaultData = {};
3845
* @param {Object} sort The request sort { field, order }, e.g. { field: 'id', order: 'DESC' }
3946
* @param {Object} filter The request filters, e.g. { body: 'hello, world' }
4047
* @param {string} referencingResource The resource name, e.g. 'posts'. Used to generate a cache key
41-
* @param {Object} options Options object to pass to the dataProvider. May include side effects to be executed upon success or failure, e.g. { onSuccess: { refresh: true } }
48+
* @param {Object} options Options object to pass to the dataProvider.
49+
* @param {boolean} options.enabled Flag to conditionally run the query. If it's false, the query will not run
50+
* @param {Function} options.onSuccess Side effect function to be executed upon success, e.g. { onSuccess: { refresh: true } }
51+
* @param {Function} options.onFailure Side effect function to be executed upon failure, e.g. { onFailure: error => notify(error.message) }
4252
*
4353
* @returns The current request state. Destructure as { data, total, ids, error, loading, loaded }.
4454
*
@@ -71,7 +81,7 @@ const useGetManyReference = (
7181
sort: SortPayload,
7282
filter: object,
7383
referencingResource: string,
74-
options?: any
84+
options?: UseGetManyReferenceOptions
7585
) => {
7686
const relatedTo = useMemo(
7787
() => nameRelatedTo(resource, id, referencingResource, target, filter),

packages/ra-core/src/dataProvider/useGetMatching.ts

+12-2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ import {
1616
getPossibleReferences,
1717
} from '../reducer';
1818

19+
interface UseGetMatchingOptions {
20+
onSuccess?: (args?: any) => void;
21+
onFailure?: (error: any) => void;
22+
enabled?: boolean;
23+
[key: string]: any;
24+
}
25+
1926
const referenceSource = (resource, source) => `${resource}@${source}`;
2027

2128
/**
@@ -41,7 +48,10 @@ const referenceSource = (resource, source) => `${resource}@${source}`;
4148
* @param {Object} filter The request filters, e.g. { title: 'hello, world' }
4249
* @param {string} source The field in resource containing the ids of the referenced records, e.g. 'tag_ids'
4350
* @param {string} referencingResource The resource name, e.g. 'posts'. Used to build a cache key
44-
* @param {Object} options Options object to pass to the dataProvider. May include side effects to be executed upon success or failure, e.g. { onSuccess: { refresh: true } }
51+
* @param {Object} options Options object to pass to the dataProvider.
52+
* @param {boolean} options.enabled Flag to conditionally run the query. If it's false, the query will not run
53+
* @param {Function} options.onSuccess Side effect function to be executed upon success, e.g. { onSuccess: { refresh: true } }
54+
* @param {Function} options.onFailure Side effect function to be executed upon failure, e.g. { onFailure: error => notify(error.message) }
4555
*
4656
* @returns The current request state. Destructure as { data, total, ids, error, loading, loaded }.
4757
*
@@ -73,7 +83,7 @@ const useGetMatching = (
7383
filter: object,
7484
source: string,
7585
referencingResource: string,
76-
options?: any
86+
options?: UseGetMatchingOptions
7787
): UseGetMatchingResult => {
7888
const relatedTo = referenceSource(referencingResource, source);
7989
const payload = { pagination, sort, filter };

packages/ra-core/src/dataProvider/useGetOne.ts

+11-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import get from 'lodash/get';
22

3-
import { Identifier, Record, ReduxState } from '../types';
3+
import {
4+
Identifier,
5+
Record,
6+
ReduxState,
7+
UseDataProviderOptions,
8+
} from '../types';
49
import useQueryWithStore from './useQueryWithStore';
510

611
/**
@@ -18,7 +23,10 @@ import useQueryWithStore from './useQueryWithStore';
1823
*
1924
* @param resource The resource name, e.g. 'posts'
2025
* @param id The resource identifier, e.g. 123
21-
* @param options Options object to pass to the dataProvider. May include side effects to be executed upon success or failure, e.g. { onSuccess: { refresh: true } }
26+
* @param {Object} options Options object to pass to the dataProvider.
27+
* @param {boolean} options.enabled Flag to conditionally run the query. If it's false, the query will not run
28+
* @param {Function} options.onSuccess Side effect function to be executed upon success, e.g. { onSuccess: { refresh: true } }
29+
* @param {Function} options.onFailure Side effect function to be executed upon failure, e.g. { onFailure: error => notify(error.message) }
2230
*
2331
* @returns The current request state. Destructure as { data, error, loading, loaded }.
2432
*
@@ -36,7 +44,7 @@ import useQueryWithStore from './useQueryWithStore';
3644
const useGetOne = <RecordType extends Record = Record>(
3745
resource: string,
3846
id: Identifier,
39-
options?: any
47+
options?: UseDataProviderOptions
4048
): UseGetOneHookValue<RecordType> =>
4149
useQueryWithStore(
4250
{ type: 'getOne', resource, payload: { id } },

packages/ra-core/src/dataProvider/useQuery.ts

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import useVersion from '../controller/useVersion';
2222
* @param {Object} query.payload The payload object, e.g; { post_id: 12 }
2323
* @param {Object} options
2424
* @param {string} options.action Redux action type
25+
* @param {boolean} options.enabled Flag to conditionally run the query. True by default. If it's false, the query will not run
2526
* @param {Function} options.onSuccess Side effect function to be executed upon success, e.g. () => refresh()
2627
* @param {Function} options.onFailure Side effect function to be executed upon failure, e.g. (error) => notify(error.message)
2728
* @param {boolean} options.withDeclarativeSideEffectsSupport Set to true to support legacy side effects e.g. { onSuccess: { refresh: true } }
@@ -145,6 +146,7 @@ export interface Query {
145146

146147
export interface QueryOptions {
147148
action?: string;
149+
enabled?: boolean;
148150
onSuccess?: OnSuccess | DeclarativeSideEffect;
149151
onFailure?: OnFailure | DeclarativeSideEffect;
150152
withDeclarativeSideEffectsSupport?: boolean;

packages/ra-core/src/dataProvider/useQueryWithStore.ts

+2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export interface QueryOptions {
2626
onSuccess?: OnSuccess;
2727
onFailure?: OnFailure;
2828
action?: string;
29+
enabled?: boolean;
2930
[key: string]: any;
3031
}
3132

@@ -79,6 +80,7 @@ const defaultIsDataLoaded = (data: any): boolean => data !== undefined;
7980
* @param {Object} query.payload The payload object, e.g; { post_id: 12 }
8081
* @param {Object} options
8182
* @param {string} options.action Redux action type
83+
* @param {boolean} options.enabled Flag to conditionally run the query. If it's false, the query will not run
8284
* @param {Function} options.onSuccess Side effect function to be executed upon success, e.g. () => refresh()
8385
* @param {Function} options.onFailure Side effect function to be executed upon failure, e.g. (error) => notify(error.message)
8486
* @param {Function} dataSelector Redux selector to get the result. Required.

packages/ra-core/src/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,7 @@ export interface UseDataProviderOptions {
297297
mutationMode?: MutationMode;
298298
onSuccess?: OnSuccess;
299299
onFailure?: OnFailure;
300+
enabled?: boolean;
300301
}
301302

302303
export type LegacyDataProvider = (

0 commit comments

Comments
 (0)