Skip to content

Commit

Permalink
Merge pull request #5849 from ValentinH/add-enabled-options-to-queries
Browse files Browse the repository at this point in the history
[RFR] Add enabled options to query hooks
  • Loading branch information
fzaninotto committed Feb 19, 2021
2 parents 541fff7 + 6207363 commit 3870e87
Show file tree
Hide file tree
Showing 12 changed files with 143 additions and 11 deletions.
20 changes: 20 additions & 0 deletions docs/Actions.md
Expand Up @@ -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()`.
Expand Down
Expand Up @@ -9,6 +9,7 @@ const OptionsProperties = [
'onSuccess',
'undoable',
'mutationMode',
'enabled',
];

const isDataProviderOptions = (value: any) => {
Expand Down
58 changes: 58 additions & 0 deletions packages/ra-core/src/dataProvider/useDataProvider.spec.js
Expand Up @@ -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 = <div data-testid="loading">loading</div>;
if (error)
content = <div data-testid="error">{error.message}</div>;
if (data)
content = (
<div data-testid="data">{JSON.stringify(data)}</div>
);
return (
<div>
{content}
<button onClick={() => setIsEnabled(e => !e)}>
toggle
</button>
</div>
);
};
const getOne = jest
.fn()
.mockResolvedValue({ data: { id: 1, title: 'foo' } });
const dataProvider = { getOne };
const { queryByTestId, getByRole } = renderWithRedux(
<DataProviderContext.Provider value={dataProvider}>
<UseGetOneWithEnabled />
</DataProviderContext.Provider>
);
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;
Expand Down
8 changes: 8 additions & 0 deletions packages/ra-core/src/dataProvider/useDataProvider.ts
Expand Up @@ -134,6 +134,7 @@ const useDataProvider = (): DataProviderProxy => {
onSuccess = undefined,
onFailure = undefined,
mutationMode = undoable ? 'undoable' : 'pessimistic',
enabled = true,
...rest
} = options || {};

Expand All @@ -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,
Expand Down
8 changes: 6 additions & 2 deletions packages/ra-core/src/dataProvider/useGetList.ts
Expand Up @@ -8,6 +8,7 @@ import {
Identifier,
Record,
RecordMap,
UseDataProviderOptions,
} from '../types';
import useQueryWithStore from './useQueryWithStore';

Expand All @@ -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 }.
*
Expand All @@ -57,7 +61,7 @@ const useGetList = <RecordType extends Record = Record>(
pagination: PaginationPayload,
sort: SortPayload,
filter: object,
options?: any
options?: UseDataProviderOptions
): {
data?: RecordMap<RecordType>;
ids?: Identifier[];
Expand Down
12 changes: 10 additions & 2 deletions packages/ra-core/src/dataProvider/useGetMany.ts
Expand Up @@ -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;
Expand Down Expand Up @@ -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 }.
*
Expand All @@ -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
Expand Down
14 changes: 12 additions & 2 deletions packages/ra-core/src/dataProvider/useGetManyReference.ts
Expand Up @@ -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.
Expand All @@ -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 }.
*
Expand Down Expand Up @@ -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),
Expand Down
14 changes: 12 additions & 2 deletions packages/ra-core/src/dataProvider/useGetMatching.ts
Expand Up @@ -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}`;

/**
Expand All @@ -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 }.
*
Expand Down Expand Up @@ -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 };
Expand Down
14 changes: 11 additions & 3 deletions 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';

/**
Expand All @@ -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 }.
*
Expand All @@ -36,7 +44,7 @@ import useQueryWithStore from './useQueryWithStore';
const useGetOne = <RecordType extends Record = Record>(
resource: string,
id: Identifier,
options?: any
options?: UseDataProviderOptions
): UseGetOneHookValue<RecordType> =>
useQueryWithStore(
{ type: 'getOne', resource, payload: { id } },
Expand Down
2 changes: 2 additions & 0 deletions packages/ra-core/src/dataProvider/useQuery.ts
Expand Up @@ -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 } }
Expand Down Expand Up @@ -145,6 +146,7 @@ export interface Query {

export interface QueryOptions {
action?: string;
enabled?: boolean;
onSuccess?: OnSuccess | DeclarativeSideEffect;
onFailure?: OnFailure | DeclarativeSideEffect;
withDeclarativeSideEffectsSupport?: boolean;
Expand Down
2 changes: 2 additions & 0 deletions packages/ra-core/src/dataProvider/useQueryWithStore.ts
Expand Up @@ -26,6 +26,7 @@ export interface QueryOptions {
onSuccess?: OnSuccess;
onFailure?: OnFailure;
action?: string;
enabled?: boolean;
[key: string]: any;
}

Expand Down Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions packages/ra-core/src/types.ts
Expand Up @@ -297,6 +297,7 @@ export interface UseDataProviderOptions {
mutationMode?: MutationMode;
onSuccess?: OnSuccess;
onFailure?: OnFailure;
enabled?: boolean;
}

export type LegacyDataProvider = (
Expand Down

0 comments on commit 3870e87

Please sign in to comment.