diff --git a/docs/Actions.md b/docs/Actions.md
index 28845b4aff3..85fd69d71f6 100644
--- a/docs/Actions.md
+++ b/docs/Actions.md
@@ -408,6 +408,26 @@ const BulkDeletePostsButton = ({ selectedIds }) => {
};
```
+## Synchronizing Dependant Queries
+`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:
+```jsx
+// fetch posts
+const { ids, data: posts, loading: isLoading } = useGetList(
+ 'posts',
+ { page: 1, perPage: 20 },
+ { field: 'name', order: 'ASC' },
+ {}
+);
+
+// then fetch categories for these posts
+const { data: categories, loading: isLoadingCategories } = useGetMany(
+ 'categories',
+ ids.map(id=> posts[id].category_id),
+ // run only if the first query returns non-empty result
+ { enabled: ids.length > 0 }
+);
+```
+
## Handling Side Effects In `useDataProvider`
`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()`.
diff --git a/packages/ra-core/src/dataProvider/getDataProviderCallArguments.ts b/packages/ra-core/src/dataProvider/getDataProviderCallArguments.ts
index 68f35a1b94e..ff7c01a92ae 100644
--- a/packages/ra-core/src/dataProvider/getDataProviderCallArguments.ts
+++ b/packages/ra-core/src/dataProvider/getDataProviderCallArguments.ts
@@ -9,6 +9,7 @@ const OptionsProperties = [
'onSuccess',
'undoable',
'mutationMode',
+ 'enabled',
];
const isDataProviderOptions = (value: any) => {
diff --git a/packages/ra-core/src/dataProvider/useDataProvider.spec.js b/packages/ra-core/src/dataProvider/useDataProvider.spec.js
index 906dc6b4e60..a25010dca66 100644
--- a/packages/ra-core/src/dataProvider/useDataProvider.spec.js
+++ b/packages/ra-core/src/dataProvider/useDataProvider.spec.js
@@ -319,6 +319,64 @@ describe('useDataProvider', () => {
expect(onFailure.mock.calls[0][0]).toEqual(new Error('foo'));
});
+ it('should accept an enabled option to block the query until a condition is met', async () => {
+ const UseGetOneWithEnabled = () => {
+ const [data, setData] = useState();
+ const [error, setError] = useState();
+ const [isEnabled, setIsEnabled] = useState(false);
+ const dataProvider = useDataProvider();
+ useEffect(() => {
+ dataProvider
+ .getOne('dummy', {}, { enabled: isEnabled })
+ .then(res => setData(res.data))
+ .catch(e => setError(e));
+ }, [dataProvider, isEnabled]);
+
+ let content =
loading
;
+ if (error)
+ content =
{error.message}
;
+ if (data)
+ content = (
+
{JSON.stringify(data)}
+ );
+ return (
+
+ {content}
+
+
+ );
+ };
+ const getOne = jest
+ .fn()
+ .mockResolvedValue({ data: { id: 1, title: 'foo' } });
+ const dataProvider = { getOne };
+ const { queryByTestId, getByRole } = renderWithRedux(
+
+
+
+ );
+ expect(queryByTestId('loading')).not.toBeNull();
+ await act(async () => {
+ await new Promise(resolve => setTimeout(resolve));
+ });
+ expect(getOne).not.toBeCalled();
+ expect(queryByTestId('loading')).not.toBeNull();
+
+ // enable the query
+ fireEvent.click(getByRole('button', { name: 'toggle' }));
+
+ await act(async () => {
+ await new Promise(resolve => setTimeout(resolve));
+ });
+ expect(getOne).toBeCalledTimes(1);
+ expect(queryByTestId('loading')).toBeNull();
+ expect(queryByTestId('data').textContent).toBe(
+ '{"id":1,"title":"foo"}'
+ );
+ });
+
describe('mutationMode', () => {
it('should wait for response to dispatch side effects in pessimistic mode', async () => {
let resolveUpdate;
diff --git a/packages/ra-core/src/dataProvider/useDataProvider.ts b/packages/ra-core/src/dataProvider/useDataProvider.ts
index e8f17b3ef9c..e24722a40f5 100644
--- a/packages/ra-core/src/dataProvider/useDataProvider.ts
+++ b/packages/ra-core/src/dataProvider/useDataProvider.ts
@@ -134,6 +134,7 @@ const useDataProvider = (): DataProviderProxy => {
onSuccess = undefined,
onFailure = undefined,
mutationMode = undoable ? 'undoable' : 'pessimistic',
+ enabled = true,
...rest
} = options || {};
@@ -157,6 +158,13 @@ const useDataProvider = (): DataProviderProxy => {
'You must pass an onSuccess callback calling notify() to use the undoable mode'
);
}
+ if (typeof enabled !== 'boolean') {
+ throw new Error('The enabled option must be a boolean');
+ }
+
+ if (enabled === false) {
+ return Promise.resolve({});
+ }
const params = {
resource,
diff --git a/packages/ra-core/src/dataProvider/useGetList.ts b/packages/ra-core/src/dataProvider/useGetList.ts
index 6b467a245aa..59489f1fa6f 100644
--- a/packages/ra-core/src/dataProvider/useGetList.ts
+++ b/packages/ra-core/src/dataProvider/useGetList.ts
@@ -8,6 +8,7 @@ import {
Identifier,
Record,
RecordMap,
+ UseDataProviderOptions,
} from '../types';
import useQueryWithStore from './useQueryWithStore';
@@ -31,7 +32,10 @@ const defaultData = {};
* @param {Object} pagination The request pagination { page, perPage }, e.g. { page: 1, perPage: 10 }
* @param {Object} sort The request sort { field, order }, e.g. { field: 'id', order: 'DESC' }
* @param {Object} filter The request filters, e.g. { title: 'hello, world' }
- * @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 } }
+ * @param {Object} options Options object to pass to the dataProvider.
+ * @param {boolean} options.enabled Flag to conditionally run the query. If it's false, the query will not run
+ * @param {Function} options.onSuccess Side effect function to be executed upon success, e.g. { onSuccess: { refresh: true } }
+ * @param {Function} options.onFailure Side effect function to be executed upon failure, e.g. { onFailure: error => notify(error.message) }
*
* @returns The current request state. Destructure as { data, total, ids, error, loading, loaded }.
*
@@ -57,7 +61,7 @@ const useGetList = (
pagination: PaginationPayload,
sort: SortPayload,
filter: object,
- options?: any
+ options?: UseDataProviderOptions
): {
data?: RecordMap;
ids?: Identifier[];
diff --git a/packages/ra-core/src/dataProvider/useGetMany.ts b/packages/ra-core/src/dataProvider/useGetMany.ts
index 9c55b0c296a..4a2b2e0bc70 100644
--- a/packages/ra-core/src/dataProvider/useGetMany.ts
+++ b/packages/ra-core/src/dataProvider/useGetMany.ts
@@ -24,6 +24,11 @@ interface Query {
interface QueriesToCall {
[resource: string]: Query[];
}
+interface UseGetManyOptions {
+ onSuccess?: Callback;
+ onFailure?: Callback;
+ enabled?: boolean;
+}
interface UseGetManyResult {
data: Record[];
error?: any;
@@ -59,7 +64,10 @@ const DataProviderOptions = { action: CRUD_GET_MANY };
*
* @param resource The resource name, e.g. 'posts'
* @param ids The resource identifiers, e.g. [123, 456, 789]
- * @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 } }
+ * @param {Object} options Options object to pass to the dataProvider.
+ * @param {boolean} options.enabled Flag to conditionally run the query. If it's false, the query will not run
+ * @param {Function} options.onSuccess Side effect function to be executed upon success, e.g. { onSuccess: { refresh: true } }
+ * @param {Function} options.onFailure Side effect function to be executed upon failure, e.g. { onFailure: error => notify(error.message) }
*
* @returns The current request state. Destructure as { data, error, loading, loaded }.
*
@@ -83,7 +91,7 @@ const DataProviderOptions = { action: CRUD_GET_MANY };
const useGetMany = (
resource: string,
ids: Identifier[],
- options: any = {}
+ options: UseGetManyOptions = {}
): UseGetManyResult => {
// we can't use useQueryWithStore here because we're aggregating queries first
// therefore part of the useQueryWithStore logic will have to be repeated below
diff --git a/packages/ra-core/src/dataProvider/useGetManyReference.ts b/packages/ra-core/src/dataProvider/useGetManyReference.ts
index 2eb8c6bfa03..320f0bbdb5b 100644
--- a/packages/ra-core/src/dataProvider/useGetManyReference.ts
+++ b/packages/ra-core/src/dataProvider/useGetManyReference.ts
@@ -18,6 +18,13 @@ import {
const defaultIds = [];
const defaultData = {};
+interface UseGetManyReferenceOptions {
+ onSuccess?: (args?: any) => void;
+ onFailure?: (error: any) => void;
+ enabled?: boolean;
+ [key: string]: any;
+}
+
/**
* Call the dataProvider.getManyReference() method and return the resolved result
* as well as the loading state.
@@ -38,7 +45,10 @@ const defaultData = {};
* @param {Object} sort The request sort { field, order }, e.g. { field: 'id', order: 'DESC' }
* @param {Object} filter The request filters, e.g. { body: 'hello, world' }
* @param {string} referencingResource The resource name, e.g. 'posts'. Used to generate a cache key
- * @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 } }
+ * @param {Object} options Options object to pass to the dataProvider.
+ * @param {boolean} options.enabled Flag to conditionally run the query. If it's false, the query will not run
+ * @param {Function} options.onSuccess Side effect function to be executed upon success, e.g. { onSuccess: { refresh: true } }
+ * @param {Function} options.onFailure Side effect function to be executed upon failure, e.g. { onFailure: error => notify(error.message) }
*
* @returns The current request state. Destructure as { data, total, ids, error, loading, loaded }.
*
@@ -71,7 +81,7 @@ const useGetManyReference = (
sort: SortPayload,
filter: object,
referencingResource: string,
- options?: any
+ options?: UseGetManyReferenceOptions
) => {
const relatedTo = useMemo(
() => nameRelatedTo(resource, id, referencingResource, target, filter),
diff --git a/packages/ra-core/src/dataProvider/useGetMatching.ts b/packages/ra-core/src/dataProvider/useGetMatching.ts
index a1a3b016627..a44ddc14143 100644
--- a/packages/ra-core/src/dataProvider/useGetMatching.ts
+++ b/packages/ra-core/src/dataProvider/useGetMatching.ts
@@ -16,6 +16,13 @@ import {
getPossibleReferences,
} from '../reducer';
+interface UseGetMatchingOptions {
+ onSuccess?: (args?: any) => void;
+ onFailure?: (error: any) => void;
+ enabled?: boolean;
+ [key: string]: any;
+}
+
const referenceSource = (resource, source) => `${resource}@${source}`;
/**
@@ -41,7 +48,10 @@ const referenceSource = (resource, source) => `${resource}@${source}`;
* @param {Object} filter The request filters, e.g. { title: 'hello, world' }
* @param {string} source The field in resource containing the ids of the referenced records, e.g. 'tag_ids'
* @param {string} referencingResource The resource name, e.g. 'posts'. Used to build a cache key
- * @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 } }
+ * @param {Object} options Options object to pass to the dataProvider.
+ * @param {boolean} options.enabled Flag to conditionally run the query. If it's false, the query will not run
+ * @param {Function} options.onSuccess Side effect function to be executed upon success, e.g. { onSuccess: { refresh: true } }
+ * @param {Function} options.onFailure Side effect function to be executed upon failure, e.g. { onFailure: error => notify(error.message) }
*
* @returns The current request state. Destructure as { data, total, ids, error, loading, loaded }.
*
@@ -73,7 +83,7 @@ const useGetMatching = (
filter: object,
source: string,
referencingResource: string,
- options?: any
+ options?: UseGetMatchingOptions
): UseGetMatchingResult => {
const relatedTo = referenceSource(referencingResource, source);
const payload = { pagination, sort, filter };
diff --git a/packages/ra-core/src/dataProvider/useGetOne.ts b/packages/ra-core/src/dataProvider/useGetOne.ts
index 6bf60ea6d0c..952449ce90e 100644
--- a/packages/ra-core/src/dataProvider/useGetOne.ts
+++ b/packages/ra-core/src/dataProvider/useGetOne.ts
@@ -1,6 +1,11 @@
import get from 'lodash/get';
-import { Identifier, Record, ReduxState } from '../types';
+import {
+ Identifier,
+ Record,
+ ReduxState,
+ UseDataProviderOptions,
+} from '../types';
import useQueryWithStore from './useQueryWithStore';
/**
@@ -18,7 +23,10 @@ import useQueryWithStore from './useQueryWithStore';
*
* @param resource The resource name, e.g. 'posts'
* @param id The resource identifier, e.g. 123
- * @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 } }
+ * @param {Object} options Options object to pass to the dataProvider.
+ * @param {boolean} options.enabled Flag to conditionally run the query. If it's false, the query will not run
+ * @param {Function} options.onSuccess Side effect function to be executed upon success, e.g. { onSuccess: { refresh: true } }
+ * @param {Function} options.onFailure Side effect function to be executed upon failure, e.g. { onFailure: error => notify(error.message) }
*
* @returns The current request state. Destructure as { data, error, loading, loaded }.
*
@@ -36,7 +44,7 @@ import useQueryWithStore from './useQueryWithStore';
const useGetOne = (
resource: string,
id: Identifier,
- options?: any
+ options?: UseDataProviderOptions
): UseGetOneHookValue =>
useQueryWithStore(
{ type: 'getOne', resource, payload: { id } },
diff --git a/packages/ra-core/src/dataProvider/useQuery.ts b/packages/ra-core/src/dataProvider/useQuery.ts
index 7be2654e0e6..dca734da960 100644
--- a/packages/ra-core/src/dataProvider/useQuery.ts
+++ b/packages/ra-core/src/dataProvider/useQuery.ts
@@ -22,6 +22,7 @@ import useVersion from '../controller/useVersion';
* @param {Object} query.payload The payload object, e.g; { post_id: 12 }
* @param {Object} options
* @param {string} options.action Redux action type
+ * @param {boolean} options.enabled Flag to conditionally run the query. True by default. If it's false, the query will not run
* @param {Function} options.onSuccess Side effect function to be executed upon success, e.g. () => refresh()
* @param {Function} options.onFailure Side effect function to be executed upon failure, e.g. (error) => notify(error.message)
* @param {boolean} options.withDeclarativeSideEffectsSupport Set to true to support legacy side effects e.g. { onSuccess: { refresh: true } }
@@ -145,6 +146,7 @@ export interface Query {
export interface QueryOptions {
action?: string;
+ enabled?: boolean;
onSuccess?: OnSuccess | DeclarativeSideEffect;
onFailure?: OnFailure | DeclarativeSideEffect;
withDeclarativeSideEffectsSupport?: boolean;
diff --git a/packages/ra-core/src/dataProvider/useQueryWithStore.ts b/packages/ra-core/src/dataProvider/useQueryWithStore.ts
index 33777c75f3d..f525c97973a 100644
--- a/packages/ra-core/src/dataProvider/useQueryWithStore.ts
+++ b/packages/ra-core/src/dataProvider/useQueryWithStore.ts
@@ -26,6 +26,7 @@ export interface QueryOptions {
onSuccess?: OnSuccess;
onFailure?: OnFailure;
action?: string;
+ enabled?: boolean;
[key: string]: any;
}
@@ -79,6 +80,7 @@ const defaultIsDataLoaded = (data: any): boolean => data !== undefined;
* @param {Object} query.payload The payload object, e.g; { post_id: 12 }
* @param {Object} options
* @param {string} options.action Redux action type
+ * @param {boolean} options.enabled Flag to conditionally run the query. If it's false, the query will not run
* @param {Function} options.onSuccess Side effect function to be executed upon success, e.g. () => refresh()
* @param {Function} options.onFailure Side effect function to be executed upon failure, e.g. (error) => notify(error.message)
* @param {Function} dataSelector Redux selector to get the result. Required.
diff --git a/packages/ra-core/src/types.ts b/packages/ra-core/src/types.ts
index 3486a27a6ca..b6b5edb9e5e 100644
--- a/packages/ra-core/src/types.ts
+++ b/packages/ra-core/src/types.ts
@@ -297,6 +297,7 @@ export interface UseDataProviderOptions {
mutationMode?: MutationMode;
onSuccess?: OnSuccess;
onFailure?: OnFailure;
+ enabled?: boolean;
}
export type LegacyDataProvider = (