diff --git a/UPGRADE.md b/UPGRADE.md
index f349a80eb50..f118a06f2dd 100644
--- a/UPGRADE.md
+++ b/UPGRADE.md
@@ -108,6 +108,203 @@ This should be mostly transparent for you unless:
- you used `useHistory` to navigate: see [https://reactrouter.com/docs/en/v6/upgrading/v5#use-usenavigate-instead-of-usehistory](https://reactrouter.com/docs/en/v6/upgrading/v5#use-usenavigate-instead-of-usehistory) to upgrade.
- you had custom components similar to our `TabbedForm` or `TabbedShowLayout` (declaring multiple sub routes): see [https://reactrouter.com/docs/en/v6/upgrading/v5](https://reactrouter.com/docs/en/v6/upgrading/v5) to upgrade.
+## `useQuery`, `useMutation`, and `useQueryWithStore` Have Been Removed
+
+React-admin v4 uses react-query rather than Redux for data fetching. The base react-query data fetching hooks (`useQuery`, `useMutation`, and `useQueryWithStore`) are no longer necessary as their functionality is provided by react-query.
+
+If your application code uses these hooks, you have 2 ways to upgrade.
+
+If you're using `useQuery` or `useMutation` to call a regular dataProvider method (like `useGetOne`), then you can use the specialized dataProvider hooks instead:
+
+```diff
+import * as React from "react";
+-import { useQuery } from 'react-admin';
++import { useGetOne } from 'react-admin';
+import { Loading, Error } from '../ui';
+const UserProfile = ({ record }) => {
+- const { loaded, error, data } = useQuery({
+- type: 'getOne',
+- resource: 'users',
+- payload: { id: record.id }
+- });
++ const { data, isLoading, error } = useGetOne(
++ 'users',
++ { id: record.id }
++ );
+- if (!loaded) { return ; }
++ if (isLoading) { return ; }
+ if (error) { return ; }
+ return
User {data.username}
;
+};
+```
+
+If you're calling a custom dataProvider method, then you can use react-query's `useQuery` or `useMutation` instead:
+
+```diff
+-import { useMutation } from 'react-admin';
++import { useDataProvider } from 'react-admin';
++import { useMutation } from 'react-query';
+const BanUserButton = ({ userId }) => {
+- const [mutate, { loading, error, data }] = useMutation({
+- type: 'banUser',
+- payload: userId
+- });
++ const dataProvider = useDataProvider();
++ const { mutate, isLoading } = useMutation(
++ ['banUser', userId],
++ () => dataProvider.banUser(userId)
++ );
+- return mutate()} disabled={loading} />;
++ return mutate()} disabled={isLoading} />;
+};
+```
+
+Refer to [the react-query documentation](https://react-query.tanstack.com/overview) for more information.
+
+## `` and `` Have Been Removed
+
+The component version of `useQuery` and `useMutation` have been removed. Use the related hook in your components instead.
+
+```diff
+-import { Query } from 'react-admin';
++import { useGetOne } from 'react-admin';
+
+const UserProfile = ({ record }) => {
+- return (
+-
+- {({ loaded, error, data }) => {
+- if (!loaded) { return ; }
+- if (error) { return ; }
+- return User {data.username}
;
+- }}
+-
+- );
++ const { data, isLoading, error } = useGetOne(
++ 'users',
++ { id: record.id }
++ );
++ if (isLoading) { return ; }
++ if (error) { return ; }
++ return User {data.username}
;
+}
+```
+
+## `useDataProvider` No Longer Accepts Side Effects
+
+`useDataProvider` returns a wrapper around the application `dataProvider` instance. In previous react-admin versions, the wrapper methods used to accept an `options` object, allowing to pass `onSuccess` and `onFailure` callbacks. This is no longer the case - the wrapper returns an object with the same method signatures as the original `dataProvider`.
+
+If you need to call the `dataProvider` and apply side effects, use react-query's `useQuery` or `useMutation` hooks instead.
+
+```diff
+-import { useState } from 'react';
+import { useDataProvider } from 'react-admin';
++import { useMutation } from 'react-query';
+
+const BanUserButton = ({ userId }) => {
+ const dataProvider = useDataProvider();
++ const { mutate, isLoading } = useMutation();
+- const [loading, setLoading] = useState(false);
+ const handleClick = () => {
+- setLoading(true);
+- dataProvider.banUser(userId, {
+- onSuccess: () => {
+- setLoading(false);
+- console.log('User banned');
+- },
+- });
++ mutate(
++ ['banUser', userId],
++ () => dataProvider.banUser(userId),
++ { onSuccess: () => console.log('User banned') }
++ );
+ }
+- return ;
++ return ;
+};
+```
+
+Refer to [the react-query documentation](https://react-query.tanstack.com/overview) for more information.
+
+## No More Records in Redux State
+
+As Redux is no longer used for data fetching, the Redux state doesn't contain any data cached from the dataProvider anymore. If you relied on `useSelector` to get a record or a list of records, you now have to use the dataProvider hooks to get them.
+
+```diff
+-import { useSelector } from 'react-redux';
++import { useGetOne } from 'react-admin';
+
+const BookAuthor = ({ record }) => {
+- const author = useSelector(state =>
+- state.admin.resources.users.data[record.authorId]
+- );
++ const { data: author, isLoading, error } = useGetOne(
++ 'users',
++ { id: record.authorId }
++ );
++ if (isLoading) { return ; }
++ if (error) { return ; }
+ return Author {data.username}
;
+};
+```
+
+## No More Data Actions
+
+As Redux is no longer used for data fetching, react-admin doesn't dispatch Redux actions like `RA/CRUD_GET_ONE_SUCCESS` and `RA/FETCH_END`. If you relied on these actions for your custom reducers, you must now use react-query `onSuccess` callback or React's `useEffect` instead.
+
+The following actions no longer exist:
+
+- `RA/CRUD_GET_ONE`
+- `RA/CRUD_GET_ONE_LOADING`
+- `RA/CRUD_GET_ONE_FAILURE`
+- `RA/CRUD_GET_ONE_SUCCESS`
+- `RA/CRUD_GET_LIST`
+- `RA/CRUD_GET_LIST_LOADING`
+- `RA/CRUD_GET_LIST_FAILURE`
+- `RA/CRUD_GET_LIST_SUCCESS`
+- `RA/CRUD_GET_ALL`
+- `RA/CRUD_GET_ALL_LOADING`
+- `RA/CRUD_GET_ALL_FAILURE`
+- `RA/CRUD_GET_ALL_SUCCESS`
+- `RA/CRUD_GET_MANY`
+- `RA/CRUD_GET_MANY_LOADING`
+- `RA/CRUD_GET_MANY_FAILURE`
+- `RA/CRUD_GET_MANY_SUCCESS`
+- `RA/CRUD_GET_MANY_REFERENCE`
+- `RA/CRUD_GET_MANY_REFERENCE_LOADING`
+- `RA/CRUD_GET_MANY_REFERENCE_FAILURE`
+- `RA/CRUD_GET_MANY_REFERENCE_SUCCESS`
+- `RA/CRUD_CREATE`
+- `RA/CRUD_CREATE_LOADING`
+- `RA/CRUD_CREATE_FAILURE`
+- `RA/CRUD_CREATE_SUCCESS`
+- `RA/CRUD_UPDATE`
+- `RA/CRUD_UPDATE_LOADING`
+- `RA/CRUD_UPDATE_FAILURE`
+- `RA/CRUD_UPDATE_SUCCESS`
+- `RA/CRUD_UPDATE_MANY`
+- `RA/CRUD_UPDATE_MANY_LOADING`
+- `RA/CRUD_UPDATE_MANY_FAILURE`
+- `RA/CRUD_UPDATE_MANY_SUCCESS`
+- `RA/CRUD_DELETE`
+- `RA/CRUD_DELETE_LOADING`
+- `RA/CRUD_DELETE_FAILURE`
+- `RA/CRUD_DELETE_SUCCESS`
+- `RA/CRUD_DELETE_MANY`
+- `RA/CRUD_DELETE_MANY_LOADING`
+- `RA/CRUD_DELETE_MANY_FAILURE`
+- `RA/CRUD_DELETE_MANY_SUCCESS`
+- `RA/FETCH_START`
+- `RA/FETCH_END`
+- `RA/FETCH_ERROR`
+- `RA/FETCH_CANCEL`
+
+Other actions related to data fetching were also removed:
+
+- `RA/REFRESH_VIEW`
+- `RA/SET_AUTOMATIC_REFRESH`
+- `RA/START_OPTIMISTIC_MODE`
+- `RA/STOP_OPTIMISTIC_MODE`
+
## Changed Signature Of Data Provider Hooks
Specialized data provider hooks (like `useGetOne`, `useGetList`, `useGetMany` and `useUpdate`) have a new signature. There are 2 changes:
@@ -275,6 +472,40 @@ And update the calls. If you're using TypeScript, your code won't compile until
These hooks are now powered by react-query, so the state argument contains way more than just `isLoading` (`reset`, `status`, `refetch`, etc.). Check the [`useQuery`](https://react-query.tanstack.com/reference/useQuery) and the [`useMutation`](https://react-query.tanstack.com/reference/useMutation) documentation on the react-query website for more details.
+## List `ids` Prop And `RecordMap` Type Are Gone
+
+Contrary to `dataProvider.getList`, `useGetList` used to return data under the shape of a record map. This is no longer the case: `useGetList` returns an array of records.
+
+So the `RecordMap` type is no longer necessary and was removed. TypeScript compilation will fail if you continue using it. You should update your code so that it works with an array of records instead.
+
+```diff
+-import { useGetList, RecordMap } from 'react-admin';
++import { useGetList, Record } from 'react-admin';
+
+const PostListContainer = () => {
+- const { data, ids, loading } = useGetList(
+- 'posts',
+- { page: 1, perPage: 10 },
+- { field: 'published_at', order: 'DESC' },
+- );
+- return loading ? null:
++ const { data, isLoading } = useGetList(
++ 'posts',
++ {
++ pagination: { page: 1, perPage: 10 },
++ sort: { field: 'published_at', order: 'DESC' },
++ }
++ );
++ return isLoading ? null:
+};
+
+-const PostListDetail = ({ ids, data }: { ids: string[], data: RecordMap }) => {
++const PostListDetail = ({ data }: { data: Record[] }) => {
+- return <>{ids.map(id => {data[id].title} )}>;
++ return <>{data.map(record => {record.title} )}>;
+};
+```
+
## Mutation Callbacks Can No Longer Be Used As Event Handlers
In 3.0, you could use a mutation callback in an event handler, e.g. a click handler on a button. This is no longer possible, so you'll have to call the callback manually inside a handler function:
@@ -319,7 +550,7 @@ const IncreaseLikeButton = ({ record }) => {
If you need to override the success or failure side effects of a component, you now have to use the `queryOptions` (for query side effects) or `mutationOptions` (for mutation side effects).
-For instance, here is how to override the side eggects for the `getOne` query in a `` component:
+For instance, here is how to override the side effects for the `getOne` query in a `` component:
```diff
const PostShow = () => {
@@ -485,6 +716,99 @@ const PostEdit = () => {
};
```
+## The `useVersion` Hook Was Removed
+
+React-admin v3 relied on a global `version` variable stored in the Redux state to force page refresh. This is no longer the case, as the refresh functionality is handled by react-query.
+
+If you relied on `useVersion` to provide a component key, you can safely remove the call. The refresh button will force all components relying on a dataProvider query to re-execute.
+
+```diff
+-import { useVersion } from 'react-admin';
+
+const MyComponent = () => {
+- const version = useVersion();
+ return (
+-
++
+ ...
+
+ );
+};
+```
+
+And if you relied on a `version` prop to be available in a page context, you can safely remove it.
+
+```diff
+import { useShowContext } from 'react-admin';
+
+const PostDetail = () => {
+- const { data, version } = useShowContext();
++ const { data } = useShowContext();
+ return (
+-
++
+ ...
+
+ );
+}
+```
+
+## Application Cache No Longer Uses `validUntil`
+
+React-admin's *application cache* used to reply on the dataProvider returning a `validUntil` property in the response. This is no longer the case, as the cache functionality is handled by react-query. Therefore, you can safely remove the `validUntil` property from your dataProvider response.
+
+```diff
+const dataProvider = {
+ getOne: (resource, { id }) => {
+ return fetch(`/api/${resource}/${id}`)
+ .then(r => r.json())
+ .then(data => {
+- const validUntil = new Date();
+- validUntil.setTime(validUntil.getTime() + 5 * 60 * 1000);
+ return {
+ data,
+- validUntil
+ };
+ }));
+ }
+};
+```
+
+This also implies that the `cacheDataProviderProxy` function was removed.
+
+```diff
+// in src/dataProvider.js
+import simpleRestProvider from 'ra-data-simple-rest';
+-import { cacheDataProviderProxy } from 'react-admin';
+
+const dataProvider = simpleRestProvider('http://path.to.my.api/');
+
+-export default cacheDataProviderProxy(dataProvider);
++export default dataProvider;
+```
+
+Instead, you must set up the cache duration at the react-query QueryClient level:
+
+```jsx
+import { QueryClient } from 'react-query';
+import { Admin, Resource } from 'react-admin';
+
+const App = () => {
+ const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ staleTime: 5 * 60 * 1000, // 5 minutes
+ },
+ },
+ });
+ return (
+
+
+
+ );
+}
+```
+
## No More Prop Injection In Page Components
Page components (``, ``, etc.) used to expect to receive props (route parameters, permissions, resource name). These components don't receive any props anymore by default. They use hooks to get the props they need from contexts or route state.
diff --git a/docs/Actions.md b/docs/Actions.md
index 15bf9a29f34..7d1da6baba6 100644
--- a/docs/Actions.md
+++ b/docs/Actions.md
@@ -9,16 +9,16 @@ Admin interfaces often have to query the API beyond CRUD requests. For instance,
React-admin provides special hooks to emit read and write queries to the [`dataProvider`](./DataProviders.md), which in turn sends requests to your API.
-## `useDataProvider` Hook
+## `useDataProvider`
React-admin stores the `dataProvider` object in a React context, so it's available from anywhere in your application code. The `useDataProvider` hook exposes the Data Provider to let you call it directly.
For instance, here is how to query the Data Provider for the current user profile:
```jsx
-import * as React from 'react';
import { useState, useEffect } from 'react';
-import { useDataProvider, Loading, Error } from 'react-admin';
+import { useDataProvider } from 'react-admin';
+import { Loading, Error } from './MyComponents';
const UserProfile = ({ userId }) => {
const dataProvider = useDataProvider();
@@ -50,45 +50,9 @@ const UserProfile = ({ userId }) => {
};
```
-**Tip**: The `dataProvider` returned by the hook is actually a *wrapper* around your Data Provider. This wrapper updates the Redux store on success, and keeps track of the loading state. In case you don't want to update the Redux store (e.g. when implementing an autosave feature), you should access the raw, non-wrapped Data Provider from the `DataProviderContext`:
-
-```diff
-import * as React from 'react';
--import { useState, useEffect } from 'react';
-+import { useState, useEffect, useContext } from 'react';
--import { useDataProvider, Loading, Error } from 'react-admin';
-+import { DataProviderContext, Loading, Error } from 'react-admin';
-
-const UserProfile = ({ userId }) => {
-- const dataProvider = useDataProvider();
-+ const dataProvider = useContext(DataProviderContext);
- const [user, setUser] = useState();
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState();
- useEffect(() => {
- dataProvider.getOne('users', { id: userId })
- .then(({ data }) => {
- setUser(data);
- setLoading(false);
- })
- .catch(error => {
- setError(error);
- setLoading(false);
- })
- }, []);
-
- if (loading) return ;
- if (error) return ;
- if (!user) return null;
+But the recommended way to query the Data Provider is to use the dataProvider method hooks (like `useGetOne`, see below).
- return (
-
- Name: {user.name}
- Email: {user.email}
-
- )
-};
-```
+**Tip**: The `dataProvider` returned by the hook is actually a *wrapper* around your Data Provider. This wrapper logs the user out if the dataProvider returns an error, and if the authProvider sees that error as an authentication error (via `authProvider.checkError()`).
**Tip**: If you use TypeScript, you can specify a record type for more type safety:
@@ -100,179 +64,59 @@ dataProvider.getOne('users', { id: 123 })
})
```
-## `useQuery` Hook
+## DataProvider Method Hooks
-The `useQuery` hook calls the Data Provider on mount, and returns an object that updates as the response arrives. It reduces the boilerplate code for calling the Data Provider.
+React-admin provides one hook for each of the Data Provider methods. They are useful shortcuts that make your code more readable and more robust.
-For instance, the previous code snippet can be rewritten with `useQuery` as follows:
+Their signature is the same as the related dataProvider method, e.g.:
```jsx
-import * as React from "react";
-import { useQuery, Loading, Error } from 'react-admin';
+useGetOne(resource, { id }); // calls dataProvider.getOne(resource, { id })
+```
+
+The previous example greatly benefits from the `useGetOne` hook, which handles loading and error states, and offers a concise way to call the Data Provider:
+
+```jsx
+import { useGetOne } from 'react-admin';
+import { Loading, Error } from './MyComponents';
const UserProfile = ({ userId }) => {
- const { data, loading, error } = useQuery({
- type: 'getOne',
- resource: 'users',
- payload: { id: userId }
- });
+ const { data: user, isLoading, error } = useGetOne('users', { id: userId });
- if (loading) return ;
+ if (isLoading) return ;
if (error) return ;
- if (!data) return null;
+ if (!user) return null;
return (
- Name: {data.name}
- Email: {data.email}
+ Name: {user.name}
+ Email: {user.email}
)
};
```
-`useQuery` expects a Query argument with the following keys:
-
-- `type`: The method to call on the Data Provider, e.g. `getList`
-- `resource`: The Resource name, e.g. "posts"
-- `payload`: The query parameters. Depends on the query type.
-
-The return value of `useQuery` is an object representing the query state, using the following keys:
-
-- `data`: `undefined` until the response arrives, then contains the `data` key in the `dataProvider` response
-- `total`: `null` until the response arrives, then contains the `total` key in the `dataProvider` response (only for `getList` and `getManyReference` types)
-- `error`: `null` unless the `dataProvider` threw an error, in which case it contains that error.
-- `loading`: A boolean updating according to the request state
-- `loaded`: A boolean updating according to the request state
-- `refetch`: A function you can call to trigger a refetch. It's different from the `refresh` function returned by `useRefresh` as it won't trigger a refresh of the view, only this specific query.
-
-This object updates according to the request state:
-
-- start: `{ loading: true, loaded: false, refetch }`
-- success: `{ data: [data from response], total: [total from response], loading: false, loaded: true, refetch }`
-- error: `{ error: [error from response], loading: false, loaded: false, refetch }`
-
-As a reminder, here are the read query types handled by Data Providers:
-
-| Type | Usage | Params format | Response format |
-| ------------------ | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
-| `getList` | Search for resources | `{ pagination: { page: {int} , perPage: {int} }, sort: { field: {string}, order: {string} }, filter: {Object} }` | `{ data: {Record[]}, total: {int} }` |
-| `getOne` | Read a single resource, by id | `{ id: {mixed} }` | `{ data: {Record} }` |
-| `getMany` | Read a list of resource, by ids | `{ ids: {mixed[]} }` | `{ data: {Record[]} }` |
-| `getManyReference` | Read a list of resources related to another one | `{ target: {string}, id: {mixed}, pagination: { page: {int} , perPage: {int} }, sort: { field: {string}, order: {string} }, filter: {Object} }` | `{ data: {Record[]} }` |
-
-## `useQueryWithStore` Hook
-
-React-admin exposes a more powerful version of `useQuery`. `useQueryWithStore` persist the response from the `dataProvider` in the internal react-admin Redux store, so that result remains available if the hook is called again in the future.
-
-You can use this hook to show the cached result immediately on mount, while the updated result is fetched from the API. This is called optimistic rendering.
-
-```diff
-import * as React from "react";
--import { useQuery, Loading, Error } from 'react-admin';
-+import { useQueryWithStore, Loading, Error } from 'react-admin';
-
-const UserProfile = ({ record }) => {
-- const { loaded, error, data } = useQuery({
-+ const { loaded, error, data } = useQueryWithStore({
- type: 'getOne',
- resource: 'users',
- payload: { id: record.id }
- });
- if (!loaded) { return ; }
- if (error) { return ; }
- return User {data.username}
;
-};
-```
-
-In practice, react-admin uses `useQueryWithStore` instead of `useQuery` everywhere, and you should probably do the same in your components. It really improves the User Experience, with only one little drawback: if the data changed on the backend side between two calls for the same query, the user may briefly see outdated data before the screen updates with the up-to-date data.
-
-Just like `useQuery`, `useQueryWithStore` also returns a `refetch` function you can call to trigger a refetch. It's different from the `refresh` function returned by `useRefresh` as it won't trigger a refresh of the view, only this specific query.
-
-## `useMutation` Hook
-
-`useQuery` emits the request to the `dataProvider` as soon as the component mounts. To emit the request based on a user action, use the `useMutation` hook instead. This hook takes the same arguments as `useQuery`, but returns a callback that emits the request when executed.
-
-Here is an implementation of an "Approve" button:
-
-```jsx
-import * as React from "react";
-import { useMutation, Button } from 'react-admin';
-
-const ApproveButton = ({ record }) => {
- const [approve, { loading }] = useMutation({
- type: 'update',
- resource: 'comments',
- payload: { id: record.id, data: { isApproved: true } }
- });
- return ;
-};
-```
-
-`useMutation` expects a Query argument with the following keys:
-
-- `type`: The method to call on the Data Provider, e.g. `update`
-- `resource`: The Resource name, e.g. "posts"
-- `payload`: The query parameters. Depends on the query type.
-
-The return value of `useMutation` is an array with the following items:
-
-- A callback function
-- An object representing the query state, using the following keys
- - `data`: `undefined` until the response arrives, then contains the `data` key in the `dataProvider` response
- - `error`: `null` unless the `dataProvider` threw an error, in which case it contains that error.
- - `loading`: A boolean updating according to the request state
- - `loaded`: A boolean updating according to the request state
-
-This object updates according to the request state:
-
-- mount: `{ loading: false, loaded: false }`
-- mutate called: `{ loading: true, loaded: false }`
-- success: `{ data: [data from response], total: [total from response], loading: false, loaded: true }`
-- error: `{ error: [error from response], loading: false, loaded: false }`
-
-You can destructure the return value of the `useMutation` hook as `[mutate, { data, total, error, loading, loaded }]`.
-
-As a reminder, here are the write query types handled by data providers:
-
-| Type | Usage | Params format | Response format |
-| ------------ | ------------------------- | --------------------------------------------------------- | ----------------------------------------------------- |
-| `create` | Create a single resource | `{ data: {Object} }` | `{ data: {Record} }` |
-| `update` | Update a single resource | `{ id: {mixed}, data: {Object}, previousData: {Object} }` | `{ data: {Record} }` |
-| `updateMany` | Update multiple resources | `{ ids: {mixed[]}, data: {Object} }` | `{ data: {mixed[]} }` The ids which have been updated |
-| `delete` | Delete a single resource | `{ id: {mixed}, previousData: {Object} }` | `{ data: {Record} }` |
-| `deleteMany` | Delete multiple resources | `{ ids: {mixed[]} }` | `{ data: {mixed[]} }` The ids which have been deleted |
-
-`useMutation` accepts a variant call where the parameters are passed to the callback instead of when calling the hook. Use this variant when some parameters are only known at call time.
+**Tip**: If you use TypeScript, you can specify the record type for more type safety:
```jsx
-import * as React from "react";
-import { useMutation, Button } from 'react-admin';
-
-const ApproveButton = ({ record }) => {
- const [mutate, { loading }] = useMutation();
- const approve = event => mutate({
- type: 'update',
- resource: 'comments',
- payload: {
- id: event.target.dataset.id,
- data: { isApproved: true, updatedAt: new Date() }
- },
- });
- return ;
-};
+const { data, isLoading } = useGetOne('products', { id: 123 });
+// \- type of data is Product
```
-**Tip**: In the example above, the callback returned by `useMutation` accepts a Query parameter. But in the previous example, it was called with a DOM Event as parameter (because it was passed directly as `onClick` handler). `useMutation` is smart enough to ignore a call time argument if it's an instance of `Event`.
+The query hooks execute on mount. They return an object with the following properties: `{ data, isLoading, error }`. Query hooks are:
-**Tip**: User actions usually trigger write queries - that's why this hook is called `useMutation`.
+* [`useGetList`](#usegetlist)
+* [`useGetOne`](#usegetone)
+* [`useGetMany`](#usegetmany)
+* [`useGetManyReference`](#usegetmanyreference)
-## Specialized Hooks
+The mutation hooks execute the query when you call a callback. They return an array with the following items: `[mutate, { data, isLoading, error }]`. Mutation hooks are:
-React-admin provides one hook for each of the Data Provider methods. Based on `useQuery` and `useMutation`, they are useful shortcuts that make your code more readable and more robust (no more method name passed as string).
+* [`useCreate`](#usecreate)
+* [`useUpdate`](#useupdate)
+* [`useUpdateMany`](#useupdatemany)
+* [`useDelete`](#usedelete)
+* [`useDeleteMany`](#usedeletemany)
For instance, here is an example using `useUpdate()`:
@@ -286,22 +130,27 @@ const ApproveButton = ({ record }) => {
};
```
-The specialized hooks based on `useQuery` (`useGetList`, `useGetOne`, `useGetMany`, `useGetManyReference`) execute on mount. The specialized hooks based on `useMutation` (`useCreate`, `useUpdate`, `useUpdateMany`, `useDelete`, `useDeleteMany`) return a callback.
-
-**Tip**: If you use TypeScript, you can specify the record type for more type safety:
+Both the query and mutation hooks accept an `options` argument, to override the query options:
```jsx
-const { data, isLoading } = useGetOne('products', { id: 123 });
-// \- type of data is Product
+const { data: user, isLoading, error } = useGetOne(
+ 'users',
+ { id: userId },
+ { enabled: userId !== undefined }
+);
```
-### `useGetList`
+## `useGetList`
-This hook calls `dataProvider.getList()` when the component mounts.
+This hook calls `dataProvider.getList()` when the component mounts. It's ideal for getting a list of records. It supports filtering, sorting, and pagination.
```jsx
// syntax
-const { data, total, isFetching, isLoading, error, refetch } = useGetList(resource, { pagination, sort, filter }, options);
+const { data, total, isLoading, error, refetch } = useGetList(
+ resource,
+ { pagination, sort, filter },
+ options
+);
// example
import { useGetList } from 'react-admin';
@@ -323,13 +172,17 @@ const LatestNews = () => {
};
```
-### `useGetOne`
+## `useGetOne`
-This hook calls `dataProvider.getOne()` when the component mounts.
+This hook calls `dataProvider.getOne()` when the component mounts. It queries the data provider for a single record, based on its `id`.
```jsx
// syntax
-const { data, isFetching, isLoading, error, refetch } = useGetOne(resource, { id }, options);
+const { data, isLoading, error, refetch } = useGetOne(
+ resource,
+ { id },
+ options
+);
// example
import { useGetOne } from 'react-admin';
@@ -342,17 +195,26 @@ const UserProfile = ({ record }) => {
};
```
-### `useGetMany`
+## `useGetMany`
+
+This hook calls `dataProvider.getMany()` when the component mounts. It queries the data provider for several records, based on an array of `ids`.
```jsx
// syntax
-const { data, isFetching, isLoading, error, refetch } = useGetMany(resource, { ids }, options);
+const { data, isLoading, error, refetch } = useGetMany(
+ resource,
+ { ids },
+ options
+);
// example
import { useGetMany } from 'react-admin';
const PostTags = ({ record }) => {
- const { data, isLoading, error } = useGetMany('tags', { ids: record.tagIds });
+ const { data, isLoading, error } = useGetMany(
+ 'tags',
+ { ids: record.tagIds }
+ );
if (isLoading) { return ; }
if (error) { return ERROR
; }
return (
@@ -365,11 +227,17 @@ const PostTags = ({ record }) => {
};
```
-### `useGetManyReference`
+## `useGetManyReference`
+
+This hook calls `dataProvider.getManyReference()` when the component mounts. It queries the data provider for a list of records related to another one (e.g. all the comments for a post). It supports filtering, sorting, and pagination.
```jsx
// syntax
-const { data, total, isFetching, isLoading, error, refetch } = useGetManyReference(resource, { target, id, pagination, sort, filter }, options);
+const { data, total, isLoading, error, refetch } = useGetManyReference(
+ resource,
+ { target, id, pagination, sort, filter },
+ options
+);
// example
import { useGetManyReference } from 'react-admin';
@@ -397,211 +265,495 @@ const PostComments = ({ record }) => {
};
```
-### `useCreate`
+## `useCreate`
+
+This hook allows to call `dataProvider.create()` when the callback is executed.
```jsx
// syntax
-const [create, { data, isFetching, isLoading, error }] = useCreate(resource, { data }, options);
+const [create, { data, isLoading, error }] = useCreate(
+ resource,
+ { data },
+ options
+);
```
The `create()` method can be called with the same parameters as the hook:
```jsx
-create(resource, { data }, options);
+create(
+ resource,
+ { data },
+ options
+);
```
+So, should you pass the parameters when calling the hook, or when executing the callback? It's up to you; but if you have the choice, we recommend passing the parameters when calling the hook (second example below).
```jsx
-// set params when calling the update callback
+// set params when calling the hook
import { useCreate } from 'react-admin';
const LikeButton = ({ record }) => {
const like = { postId: record.id };
- const [create, { isLoading, error }] = useCreate();
+ const [create, { isLoading, error }] = useCreate('likes', { data: like });
const handleClick = () => {
- create('likes', { data: like })
+ create()
}
if (error) { return ERROR
; }
return Like ;
};
-// set params when calling the hook
+// set params when calling the create callback
import { useCreate } from 'react-admin';
const LikeButton = ({ record }) => {
const like = { postId: record.id };
- const [create, { isLoading, error }] = useCreate('likes', { data: like });
+ const [create, { isLoading, error }] = useCreate();
const handleClick = () => {
- create()
+ create('likes', { data: like })
}
if (error) { return ERROR
; }
return Like ;
};
```
-### `useUpdate`
+## `useUpdate`
+
+This hook allows to call `dataProvider.update()` when the callback is executed, and update a single record based on its `id` and a `data` argument.
```jsx
// syntax
-const [update, { data, isLoading, error }] = useUpdate(resource, { id, data, previousData }, options);
+const [update, { data, isLoading, error }] = useUpdate(
+ resource,
+ { id, data, previousData },
+ options
+);
```
The `update()` method can be called with the same parameters as the hook:
```jsx
-update(resource, { id, data, previousData }, options);
+update(
+ resource,
+ { id, data, previousData },
+ options
+);
```
-This means the parameters can be passed either when calling the hook, or when calling the callback.
+This means the parameters can be passed either when calling the hook, or when calling the callback. It's up to you to pick the syntax that best suits your component. If you have the choice, we recommend passing the parameters when calling the hook (second example below).
```jsx
-// set params when calling the update callback
+// set params when calling the hook
import { useUpdate } from 'react-admin';
const IncreaseLikeButton = ({ record }) => {
const diff = { likes: record.likes + 1 };
- const [update, { isLoading, error }] = useUpdate();
+ const [update, { isLoading, error }] = useUpdate(
+ 'likes',
+ { id: record.id, data: diff, previousData: record }
+ );
const handleClick = () => {
- update('likes', { id: record.id, data: diff, previousData: record })
+ update()
}
if (error) { return ERROR
; }
return Like ;
};
-// or set params when calling the hook
+// set params when calling the update callback
import { useUpdate } from 'react-admin';
const IncreaseLikeButton = ({ record }) => {
const diff = { likes: record.likes + 1 };
- const [update, { isLoading, error }] = useUpdate('likes', { id: record.id, data: diff, previousData: record });
+ const [update, { isLoading, error }] = useUpdate();
const handleClick = () => {
- update()
+ update(
+ 'likes',
+ { id: record.id, data: diff, previousData: record }
+ )
}
if (error) { return ERROR
; }
return Like ;
};
```
-### `useUpdateMany`
+## `useUpdateMany`
+
+This hook allows to call `dataProvider.updateMany()` when the callback is executed, and update an array of records based on their `ids` and a `data` argument.
+
```jsx
// syntax
-const [updateMany, { data, isFetching, isLoading, error }] = useUpdateMany(resource, { ids, data }, options);
+const [updateMany, { data, isLoading, error }] = useUpdateMany(
+ resource,
+ { ids, data },
+ options
+);
```
The `updateMany()` method can be called with the same parameters as the hook:
```jsx
-updateMany(resource, { ids, data }, options);
+updateMany(
+ resource,
+ { ids, data },
+ options
+);
```
+So, should you pass the parameters when calling the hook, or when executing the callback? It's up to you; but if you have the choice, we recommend passing the parameters when calling the hook (second example below).
+
```jsx
-// set params when calling the updateMany callback
+// set params when calling the hook
import { useUpdateMany } from 'react-admin';
const BulkResetViewsButton = ({ selectedIds }) => {
- const [updateMany, { isLoading, error }] = useUpdateMany();
+ const [updateMany, { isLoading, error }] = useUpdateMany(
+ 'posts',
+ { ids: selectedIds, data: { views: 0 } }
+ );
const handleClick = () => {
- updateMany('posts', { ids: selectedIds, data: { views: 0 } });
+ updateMany();
}
if (error) { return ERROR
; }
return Reset views ;
};
-// set params when calling the hook
+// set params when calling the updateMany callback
import { useUpdateMany } from 'react-admin';
const BulkResetViewsButton = ({ selectedIds }) => {
- const [updateMany, { isLoading, error }] = useUpdateMany('posts', { ids: selectedIds, data: { views: 0 } });
+ const [updateMany, { isLoading, error }] = useUpdateMany();
const handleClick = () => {
- updateMany();
+ updateMany(
+ 'posts',
+ { ids: selectedIds, data: { views: 0 } }
+ );
}
if (error) { return ERROR
; }
return Reset views ;
};
```
-### `useDelete`
+## `useDelete`
+
+This hook allows calling `dataProvider.delete()` when the callback is executed and deleting a single record based on its `id`.
```jsx
// syntax
-const [deleteOne, { data, isFetching, isLoading, error }] = useDelete(resource, { id, previousData }, options);
+const [deleteOne, { data, isLoading, error }] = useDelete(
+ resource,
+ { id, previousData },
+ options
+);
```
The `deleteOne()` method can be called with the same parameters as the hook:
```jsx
-deleteOne(resource, { id, previousData }, options);
+deleteOne(
+ resource,
+ { id, previousData },
+ options
+);
```
+So, should you pass the parameters when calling the hook, or when executing the callback? It's up to you; but if you have the choice, we recommend passing the parameters when calling the hook (second example below).
+
```jsx
-// set params when calling the deleteOne callback
+// set params when calling the hook
import { useDelete } from 'react-admin';
const DeleteButton = ({ record }) => {
- const [deleteOne, { isLoading, error }] = useDelete();
+ const [deleteOne, { isLoading, error }] = useDelete(
+ 'likes',
+ { id: record.id, previousData: record }
+ );
const handleClick = () => {
- deleteOne('likes', { id: record.id , previousData: record })
+ deleteOne();
}
if (error) { return ERROR
; }
- return Delete;
+ return Delete ;
};
-// set params when calling the hook
+// set params when calling the deleteOne callback
import { useDelete } from 'react-admin';
const DeleteButton = ({ record }) => {
- const [deleteOne, { isLoading, error }] = useDelete('likes', { id: record.id, previousData: record });
+ const [deleteOne, { isLoading, error }] = useDelete();
const handleClick = () => {
- deleteOne()
+ deleteOne(
+ 'likes',
+ { id: record.id , previousData: record }
+ );
}
if (error) { return ERROR
; }
- return Delete ;
+ return Delete;
};
```
-### `useDeleteMany`
+## `useDeleteMany`
+
+This hook allows to call `dataProvider.deleteMany()` when the callback is executed, and delete an array of records based on their `ids`.
```jsx
// syntax
-const [deleteMany, { data, isFetching, isLoading, error }] = useDeleteMany(resource, { ids }, options);
+const [deleteMany, { data, isLoading, error }] = useDeleteMany(
+ resource,
+ { ids },
+ options
+);
```
The `deleteMany()` method can be called with the same parameters as the hook:
```jsx
-deleteMany(resource, { ids }, options);
+deleteMany(
+ resource,
+ { ids },
+ options
+);
```
+So, should you pass the parameters when calling the hook, or when executing the callback? It's up to you; but if you have the choice, we recommend passing the parameters when calling the hook (second example below).
+
```jsx
-// set params when calling the dleteMany callback
+// set params when calling the hook
import { useDeleteMany } from 'react-admin';
const BulkDeletePostsButton = ({ selectedIds }) => {
- const [deleteMany, { isLoading, error }] = useDeleteMany();
+ const [deleteMany, { isLoading, error }] = useDeleteMany(
+ 'posts',
+ { ids: selectedIds }
+ );
const handleClick = () => {
- deleteMany('posts', { ids: selectedIds })
+ deleteMany()
}
if (error) { return ERROR
; }
return Delete selected posts ;
};
-// set params when calling the hook
+// set params when calling the deleteMany callback
import { useDeleteMany } from 'react-admin';
const BulkDeletePostsButton = ({ selectedIds }) => {
- const [deleteMany, { isLoading, error }] = useDeleteMany('posts', { ids: selectedIds });
+ const [deleteMany, { isLoading, error }] = useDeleteMany();
const handleClick = () => {
- deleteMany()
+ deleteMany(
+ 'posts',
+ { ids: selectedIds }
+ )
}
if (error) { return ERROR
; }
return Delete selected posts ;
};
```
-## Synchronizing Dependant Queries
+## React-query
+
+Internally, react-admin uses [react-query](https://react-query.tanstack.com/) to call the dataProvider. When fetching data from the dataProvider in your components, if you can't use any of the dataProvider method hooks, you should use that library, too. It brings several benefits:
+
+1. It triggers the loader in the AppBar when the query is running.
+2. It reduces the boilerplate code since you don't need to use `useState`.
+3. It supports a vast array of options
+3. It displays stale data while fetching up-to-date data, leading to a snappier UI
+
+React-query offers 2 main hooks to interact with the dataProvider:
+
+* [`useQuery`](https://react-query.tanstack.com/reference/useQuery): fetches the dataProvider on mount. This is for *read* queries.
+* [`useMutation`](https://react-query.tanstack.com/reference/useMutation): fetches the dataProvider when you call a callback. This is for *write* queries, and *read* queries that execute on user interaction.
+
+Both these hooks accept a query *key* (identifying the query in the cache), and a query *function* (executing the query and returning a Promise). Internally, react-admin uses an array of arguments as the query key.
+
+For instance, the initial code snippet of this chapter can be rewritten with `useQuery` as follows:
+
+```jsx
+import * as React from "react";
+import { useQuery } from 'react-query';
+import { useDataProvider, Loading, Error } from 'react-admin';
+
+const UserProfile = ({ userId }) => {
+ const dataProvider = useDataProvider();
+ const { data, isLoading, error } = useQuery(
+ ['user', 'getOne', userId],
+ () => dataProvider.getOne('users', { id: userId })
+ );
+
+ if (isLoading) return ;
+ if (error) return ;
+ if (!data) return null;
+
+ return (
+
+ Name: {data.name}
+ Email: {data.email}
+
+ )
+};
+```
+
+To illustrate the usage of `useMutation`, here is an implementation of an "Approve" button for a comment:
+
+```jsx
+import * as React from "react";
+import { useMutation } from 'react-query';
+import { useDataProvider, Button } from 'react-admin';
+
+const ApproveButton = ({ record }) => {
+ const dataProvider = useDataProvider();
+ const { mutate, isLoading } = useMutation(
+ ['comments', 'update', { id: record.id, data: { isApproved: true } }],
+ () => dataProvider.update('comments', { id: record.id, data: { isApproved: true } })
+ );
+ return mutate()} disabled={isLoading} />;
+};
+```
+
+If you want to go beyond data provider method hooks, we recommend that you read [the react-query documentation](https://react-query.tanstack.com/overview).
+
+## `isLoading` vs `isFetching`
+
+Data fetching hooks return two loading state variables: `isLoading` and `isFetching`. Which one should you use?
+
+The short answer is: use `isLoading`. Read on to understand why.
+
+The source of these two variables is [react-query](https://react-query.tanstack.com/guides/queries#query-basics). Here is how they defined these two variables:
+
+- `isLoading`: The query has no data and is currently fetching
+- `isFetching`: In any state, if the query is fetching at any time (including background refetching) isFetching will be true.
+
+Let's see how what these variables contain in a typical usage scenario:
+
+1. The user first loads a page. `isLoading` is true, and `isFetching` is also true because the data was never loaded
+2. The dataProvider returns the data. Both `isLoading` and `isFetching` become false
+3. The user navigates away
+4. The user comes back to the first page, which triggers a new fetch. `isLoading` is false, because the stale data is available, and `isFetching` is true because the dataProvider is being fetched.
+5. The dataProvider returns the data. Both `isLoading` and `isFetching` become false
+
+Components use the loading state to show a loading indicator when there is no data to show. In the example above, the loading indicator is necessary for step 2, but not in step 4, because you can display the stale data while fresh data is being loaded.
+
+```jsx
+import { useGetOne } from 'react-admin';
+
+const UserProfile = ({ record }) => {
+ const { data, isLoading, error } = useGetOne('users', { id: record.id });
+ if (isLoading) { return ; }
+ if (error) { return ERROR
; }
+ return User {data.username}
;
+};
+```
+
+As a consequence, you should always use `isLoading` to determine if you need to show a loading indicator.
+
+## Calling Custom Methods
+
+Your dataProvider may contain custom methods, e.g. for calling RPC endpoints on your API. `useQuery` and `use%Mutation` are especially useful for calling these methods.
+
+For instance, if your `dataProvider` exposes a `banUser()` method:
+
+```jsx
+const dataProvider = {
+ getList: /** ... **/,
+ getOne: /** ... **/,
+ getMany: /** ... **/,
+ getManyReference /** ... **/,
+ create: /** ... **/,
+ update: /** ... **/,
+ updateMany /** ... **/,
+ delete: /** ... **/,
+ deleteMany /** ... **/,
+ banUser: (userId) => {
+ return fetch(`/api/user/${userId}/ban`, { method: 'POST' })
+ .then(response => response.json());
+ },
+}
+```
+
+You can call it inside a `` button component as follows:
+
+```jsx
+const BanUserButton = ({ userId }) => {
+ const dataProvider = useDataProvider();
+ const { mutate, isLoading } = useMutation(
+ ['banUser', userId],
+ () => dataProvider.banUser(userId)
+ );
+ return mutate()} disabled={isLoading} />;
+};
+```
+
+## Query Options
+
+The data provider method hooks (like `useGetOne`) and react-query's hooks (like `useQuery`) accept a query options object as the last argument. This object can be used to modify the way the query is executed. There are many options, all documented [in the react-query documentation](https://react-query.tanstack.com/reference/useQuery):
+
+- `cacheTime`
+- `enabled`
+- `initialData`
+- `initialDataUpdatedA`
+- `isDataEqual`
+- `keepPreviousData`
+- `meta`
+- `notifyOnChangeProps`
+- `notifyOnChangePropsExclusions`
+- `onError`
+- `onSettled`
+- `onSuccess`
+- `queryKeyHashFn`
+- `refetchInterval`
+- `refetchIntervalInBackground`
+- `refetchOnMount`
+- `refetchOnReconnect`
+- `refetchOnWindowFocus`
+- `retry`
+- `retryOnMount`
+- `retryDelay`
+- `select`
+- `staleTime`
+- `structuralSharing`
+- `suspense`
+- `useErrorBoundary`
+
+For instance, if you want to execute a callback when the query completes (whether it's successful or failed), you can use the `onSettled` option. this can be useful e.g. to log all calls to the dataProvider:
+
+```jsx
+import { useGetOne } from 'react-admin';
+
+const UserProfile = ({ record }) => {
+ const { data, isLoading, error } = useGetOne(
+ 'users',
+ { id: record.id },
+ { onSettled: (data, error) => console.log(data, error) }
+ );
+ if (isLoading) { return ; }
+ if (error) { return ERROR
; }
+ return User {data.username}
;
+};
+```
+
+We won't re-explain all these options here, but we'll focus on the most useful ones in react-admin.
+
+**Tip**: In react-admin components that use the data provider method hooks, you can override the query options using the `queryOptions` prop, and the mutation options using the `mutationOptions` prop. For instance, to log the dataProvider calls, in the `` component, you can do the following:
+
+```jsx
+import { List; Datagrid, TextField } from 'react-admin';
+
+const PostList = () => (
+ console.log(data, error) }}
+ >
+
+
+
+
+
+
+);
+```
+
+## Synchronizing Dependent 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, the following code only fetches the categories if at least one post is already loaded:
-`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 { data: posts, isLoading } = useGetList(
@@ -612,43 +764,46 @@ const { data: posts, isLoading } = useGetList(
// then fetch categories for these posts
const { data: categories, isLoading: isLoadingCategories } = useGetMany(
'categories',
- posts.map(post => posts.category_id),
+ { ids: posts.map(post => posts.category_id) },
// run only if the first query returns non-empty result
{ enabled: !isLoading && posts.length > 0 }
);
```
-## Handling Side Effects In `useDataProvider`
+## Success and Error Side Effects
-`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()`.
+To execute some logic after a query or a mutation is complete, use the `onSuccess` and `onError` options. React-admin uses the term "side effects" for this type of logic, as it's usually modifying another part of the UI.
-For instance, here is another version of the `` based on `useDataProvider` that notifies the user of success or failure using the bottom notification banner:
+This is very common when using mutation hooks like `useUpdate`, e.g. to display a notification, or redirect to another page. For instance, here is an `` that notifies the user of success or failure using the bottom notification banner:
```jsx
import * as React from "react";
-import { useDataProvider, useNotify, useRedirect, Button } from 'react-admin';
+import { useUpdate, useNotify, useRedirect, Button } from 'react-admin';
const ApproveButton = ({ record }) => {
const notify = useNotify();
const redirect = useRedirect();
- const dataProvider = useDataProvider();
- const approve = () => dataProvider
- .update('comments', { id: record.id, data: { isApproved: true } })
- .then(response => {
- // success side effects go here
- redirect('/comments');
- notify('Comment approved');
- })
- .catch(error => {
- // failure side effects go here
- notify(`Comment approval error: ${error.message}`, { type: 'warning' });
- });
+ const [approve, { isLoading }] = useUpdate(
+ 'comments',
+ { id: record.id, data: { isApproved: true } }
+ {
+ onSuccess: (data) => {
+ // success side effects go here
+ redirect('/comments');
+ notify('Comment approved');
+ },
+ onError: (error) => {
+ // failure side effects go here
+ notify(`Comment approval error: ${error.message}`, { type: 'warning' });
+ },
+ }
+ );
- return ;
+ return approve()} disabled={isLoading} />;
};
```
-Fetching data is called a *side effect*, since it calls the outside world, and is asynchronous. Usual actions may have other side effects, like showing a notification, or redirecting the user to another page. React-admin provides the following hooks to handle most common side effects:
+React-admin provides the following hooks to handle the most common side effects:
- [`useNotify`](#usenotify): Return a function to display a notification.
- [`useRedirect`](#useredirect): Return a function to redirect the user to another page.
@@ -657,7 +812,7 @@ Fetching data is called a *side effect*, since it calls the outside world, and i
### `useNotify`
-This hook returns a function that displays a notification in the bottom of the page.
+This hook returns a function that displays a notification at the bottom of the page.
```jsx
import { useNotify } from 'react-admin';
@@ -671,13 +826,14 @@ const NotifyButton = () => {
};
```
-The callback takes 6 arguments:
+The callback takes 2 arguments:
- The message to display
-- The level of the notification (`info`, `success` or `warning` - the default is `info`)
-- An `options` object to pass to the `translate` function (because notification messages are translated if your admin has an `i18nProvider`). It is useful for inserting variables into the translation.
-- An `undoable` boolean. Set it to `true` if the notification should contain an "undo" button
-- A `duration` number. Set it to `0` if the notification should not be dismissible.
-- A `multiLine` boolean. Set it to `true` if the notification message should be shown in more than one line.
+- an `options` object with the following keys
+ - The `type` of the notification (`info`, `success` or `warning` - the default is `info`)
+ - A `messageArgs` object to pass to the `translate` function (because notification messages are translated if your admin has an `i18nProvider`). It is useful for inserting variables into the translation.
+ - An `undoable` boolean. Set it to `true` if the notification should contain an "undo" button
+ - An `autoHideDuration` number. Set it to `0` if the notification should not be dismissible.
+ - A `multiLine` boolean. Set it to `true` if the notification message should be shown in more than one line.
Here are more examples of `useNotify` calls:
@@ -685,23 +841,12 @@ Here are more examples of `useNotify` calls:
// notify a warning
notify(`This is a warning`, 'warning');
// pass translation arguments
-notify('item.created', 'info', { resource: 'post' });
+notify('item.created', { type: 'info', messageArgs: { resource: 'post' } });
// send an undoable notification
-notify('Element updated', 'info', undefined, true);
-```
-
-**Tip**: The callback also allows a signature with only 2 arguments, the message to display and an object with the rest of the arguments
-
-```js
-// notify an undoable success message, with translation arguments
-notify('Element deleted', {
- type: 'success',
- undoable: true,
- messageArgs: { resource: 'post' }
-});
+notify('Element updated', { type: 'info', undoable: true });
```
-**Tip**: When using `useNotify` as a side effect for an `undoable` Edit form, you MUST set the fourth argument to `true`, otherwise the "undo" button will not appear, and the actual update will never occur.
+**Tip**: When using `useNotify` as a side effect for an `undoable` mutation, you MUST set the `undoable` option to `true`, otherwise the "undo" button will not appear, and the actual update will never occur.
```jsx
import * as React from 'react';
@@ -715,7 +860,7 @@ const PostEdit = () => {
};
return (
-
+
...
@@ -744,7 +889,7 @@ The callback takes 5 arguments:
- The page to redirect the user to ('list', 'create', 'edit', 'show', a function or a custom path)
- The current `basePath`
- The `id` of the record to redirect to (if any)
- - A record like object to be passed to the first argument, when the first argument is a function
+ - A record-like object to be passed to the first argument, when the first argument is a function
- A `state` to be set to the location
Here are more examples of `useRedirect` calls:
@@ -766,11 +911,11 @@ redirect('edit', '/posts', 1, {}, { record: { post_id: record.id } });
redirect(false);
```
-Note that `useRedirect` allows redirection to an absolute url outside the current React app.
+Note that `useRedirect` allows redirection to an absolute URL outside the current React app.
### `useRefresh`
-This hook returns a function that forces a rerender of the current view.
+This hook returns a function that forces a refetch of all the active queries, and a rerender of the current view when the data has changed.
```jsx
import { useRefresh } from 'react-admin';
@@ -784,22 +929,6 @@ const RefreshButton = () => {
};
```
-To make this work, react-admin stores a `version` number in its state. The `useDataProvider()` hook uses this `version` in its effect dependencies. Also, page components use the `version` as `key`. The `refresh` callback increases the `version`, which forces a re-execution all queries based on the `useDataProvider()` hook, and a rerender of all components using the `version` as key.
-
-This means that you can make any component inside a react-admin app refreshable by using the right key:
-
-```jsx
-import * as React from 'react';
-import { useVersion } from 'react-admin';
-
-const MyComponent = () => {
- const version = useVersion();
- return
- ...
-
;
-};
-```
-
The callback takes 1 argument:
- `hard`: when set to true, the callback empties the cache, too
@@ -819,46 +948,42 @@ const UnselectAllButton = () => {
};
```
-## Handling Side Effects In Other Hooks
-
-The other hooks presented in this chapter, starting with `useQuery`, don't expose the `dataProvider` Promise. To allow for side effects with these hooks, they all accept an additional `options` argument. It's an object with `onSuccess` and `onFailure` functions, that react-admin executes on success... or on failure.
+## Optimistic Rendering and Undo
-So an `` written with `useMutation` instead of `useDataProvider` can specify side effects as follows:
+In the following example, after clicking on the "Approve" button, a loading spinner appears while the data provider is fetched. Then, users are redirected to the comments list.
```jsx
import * as React from "react";
-import { useMutation, useNotify, useRedirect, Button } from 'react-admin';
+import { useUpdate, useNotify, useRedirect, Button } from 'react-admin';
const ApproveButton = ({ record }) => {
const notify = useNotify();
const redirect = useRedirect();
- const [approve, { loading }] = useMutation(
- {
- type: 'update',
- resource: 'comments',
- payload: { id: record.id, data: { isApproved: true } },
- },
+ const [approve, { isLoading }] = useUpdate(
+ 'comments',
+ { id: record.id, data: { isApproved: true } }
{
- onSuccess: ({ data }) => {
+ onSuccess: (data) => {
redirect('/comments');
notify('Comment approved');
},
- onFailure: (error) => notify(`Comment approval error: ${error.message}`, { type: 'warning' }),
+ onError: (error) => {
+ notify(`Comment approval error: ${error.message}`, { type: 'warning' });
+ },
}
);
- return ;
+
+ return approve()} disabled={isLoading} />;
};
```
-## Optimistic Rendering and Undo
-
-In the previous example, after clicking on the "Approve" button, a loading spinner appears while the data provider is fetched. Then, users are redirected to the comments list. But in most cases, the server returns a success response, so the user waits for this response for nothing.
+But in most cases, the server returns a successful response, so the user waits for this response for nothing.
This is called **pessimistic rendering**, as all users are forced to wait because of the (usually rare) possibility of server failure.
-An alternative mode for mutations is **optimistic rendering**. The idea is to handle the calls to the `dataProvider` on the client side first (i.e. updating entities in the Redux store), and re-render the screen immediately. The user sees the effect of their action with no delay. Then, react-admin applies the success side effects, and only after that, it triggers the call to the data provider. If the fetch ends with a success, react-admin does nothing more than a refresh to grab the latest data from the server. In most cases, the user sees no difference (the data in the Redux store and the data from the `dataProvider` are the same). If the fetch fails, react-admin shows an error notification, and forces a refresh, too.
+An alternative mode for mutations is **optimistic rendering**. The idea is to handle the calls to the `dataProvider` on the client side first (i.e. updating entities in the react-query cache), and re-render the screen immediately. The user sees the effect of their action with no delay. Then, react-admin applies the success side effects, and only after that, it triggers the call to the data provider. If the fetch ends with success, react-admin does nothing more than a refresh to grab the latest data from the server. In most cases, the user sees no difference (the data in the Redux store and the data from the `dataProvider` are the same). If the fetch fails, react-admin shows an error notification and reverts the mutation.
-A third mutation mode is called **undoable**. It's like optimistic rendering, but with an added feature: after applying the changes and the side effects locally, react-admin *waits* for a few seconds before triggering the call to the `dataProvider`. During this delay, the end user sees an "undo" button that, when clicked, cancels the call to the `dataProvider` and refreshes the screen.
+A third mutation mode is called **undoable**. It's like optimistic rendering, but with an added feature: after applying the changes and the side effects locally, react-admin *waits* for a few seconds before triggering the call to the `dataProvider`. During this delay, the end-user sees an "undo" button that, when clicked, cancels the call to the `dataProvider` and refreshes the screen.
Here is a quick recap of the three mutation modes:
@@ -870,26 +995,25 @@ Here is a quick recap of the three mutation modes:
| cancellable | no | no | yes |
-By default, react-admin uses the undoable mode for the Edit view. For the Create view, react-admin needs to wait for the response to know the id of the resource to redirect to, so the mutation mode is pessimistic.
+By default, react-admin uses the `undoable` mode for the Edit view. As for the data provider method hooks, they default to the `pessimistic` mode.
+
+**Tip**: For the Create view, react-admin needs to wait for the response to know the id of the resource to redirect to, so the mutation mode is pessimistic.
-You can benefit from optimistic and undoable modes when you call the `useMutation` hook, too. You just need to pass a `mutationMode` value in the `options` parameter:
+You can benefit from optimistic and undoable modes when you call the `useUpdate` hook, too. You just need to pass a `mutationMode` option:
```diff
import * as React from "react";
-import { useMutation, useNotify, useRedirect, Button } from 'react-admin';
+import { useUpdate, useNotify, useRedirect, Button } from 'react-admin';
const ApproveButton = ({ record }) => {
const notify = useNotify();
const redirect = useRedirect();
- const [approve, { loading }] = useMutation(
- {
- type: 'update',
- resource: 'comments',
- payload: { id: record.id, data: { isApproved: true } },
- },
+ const [approve, { isLoading }] = useUpdate(
+ 'comments',
+ { id: record.id, data: { isApproved: true } }
{
+ mutationMode: 'undoable',
-- onSuccess: ({ data }) => {
+- onSuccess: (data) => {
+ onSuccess: () => {
redirect('/comments');
- notify('Comment approved');
@@ -898,212 +1022,22 @@ const ApproveButton = ({ record }) => {
onFailure: (error) => notify(`Error: ${error.message}`, { type: 'warning' }),
}
);
- return ;
+ return approve()} disabled={isLoading} />;
};
```
-As you can see in this example, you need to tweak the notification for undoable calls: passing `true` as fourth parameter of `notify` displays the 'Undo' button in the notification. Also, as side effects are executed immediately, they can't rely on the response being passed to onSuccess.
-
-You can pass the `mutationMode` option parameter to specialized hooks, too. They all accept an optional last argument with side effects.
-
-```jsx
-import * as React from "react";
-import { useUpdate, useNotify, useRedirect, Button } from 'react-admin';
+As you can see in this example, you need to tweak the notification for undoable calls: passing `undo: true` displays the 'Undo' button in the notification. Also, as side effects are executed immediately, they can't rely on the response being passed to onSuccess.
-const ApproveButton = ({ record }) => {
- const notify = useNotify();
- const redirect = useRedirect();
- const [approve, { isLoading }] = useUpdate(
- 'comments',
- { id: record.id, data: { isApproved: true }, previousData: record },
- {
- mutationMode: 'undoable',
- onSuccess: () => {
- redirect('/comments');
- notify('Comment approved', { undoable: true });
- },
- onError: (error) => notify(`Error: ${error.message}`, { type: 'warning' }),
- }
- );
- return ;
-};
-```
-
-## Customizing the Redux Action
-
-The `useDataProvider` hook dispatches redux actions on load, on success, and on error. By default, these actions are called:
-
-- `CUSTOM_FETCH_LOAD`
-- `CUSTOM_FETCH_SUCCESS`
-- `CUSTOM_FETCH_FAILURE`
-
-React-admin doesn't have any reducer watching these actions. You can write a custom reducer for these actions to store the return of the Data Provider in Redux. But the best way to do so is to set the hooks dispatch a custom action instead of `CUSTOM_FETCH`. Use the `action` option for that purpose:
-
-```diff
-import * as React from "react";
-import { useUpdate, useNotify, useRedirect, Button } from 'react-admin';
-
-const ApproveButton = ({ record }) => {
- const notify = useNotify();
- const redirect = useRedirect();
- const [approve, { isLoading }] = useUpdate(
- 'comments',
- { id: record.id, data: { isApproved: true } },
- {
-+ action: 'MY_CUSTOM_ACTION',
- mutationMode: 'undoable',
- onSuccess: ({ data }) => {
- redirect('/comments');
- notify('Comment approved', { undoable: true });
- },
- onError: (error) => notify(`Error: ${error.message}`, { type: 'warning' }),
- }
- );
- return ;
-};
-```
-
-**Tip**: When using the Data Provider hooks for regular pages (List, Edit, etc.), react-admin always specifies a custom action name, related to the component asking for the data. For instance, in the `` page, the action is called `CRUD_GET_LIST`. So unless you call the Data Provider hooks yourself, no `CUSTOM_FETCH` action should be dispatched.
-
-## Legacy Components: ``, ``, and `withDataProvider`
-
-Before React had hooks, react-admin used render props and higher order components to provide the same functionality. Legacy code will likely contain instances of ``, ``, and `withDataProvider`. Their syntax, which is identical to their hook counterpart, is illustrated below.
-
-You can fetch and display a user profile using the `` component, which uses render props:
-
-{% raw %}
-```jsx
-import * as React from "react";
-import { Query, Loading, Error } from 'react-admin';
-
-const UserProfile = ({ record }) => (
-
- {({ data, loading, error }) => {
- if (loading) { return ; }
- if (error) { return ; }
- return User {data.username}
;
- }}
-
-);
-```
-{% endraw %}
-
-Or, query a user list on the dashboard with the same `` component:
-
-```jsx
-import * as React from "react";
-import { Query, Loading, Error } from 'react-admin';
-
-const payload = {
- pagination: { page: 1, perPage: 10 },
- sort: { field: 'username', order: 'ASC' },
-};
-
-const UserList = () => (
-
- {({ data, total, loading, error }) => {
- if (loading) { return ; }
- if (error) { return ; }
- return (
-
-
Total users: {total}
-
- {data.map(user => {user.username} )}
-
-
- );
- }}
-
-);
-```
-
-Just like `useQuery`, the `` component expects three parameters: `type`, `resource`, and `payload`. It fetches the data provider on mount, and passes the data to its child component once the response from the API arrives.
-
-And if you need to chain API calls, don't hesitate to nest `` components.
-
-When calling the API to update ("mutate") data, use the `` component instead. It passes a callback to trigger the API call to its child function.
-
-Here is a version of the `` component demonstrating ``:
-
-```jsx
-import * as React from "react";
-import { Mutation, useNotify, useRedirect, Button } from 'react-admin';
-
-const ApproveButton = ({ record }) => {
- const notify = useNotify();
- const redirect = useRedirect();
- const payload = { id: record.id, data: { ...record, is_approved: true } };
- const options = {
- mutationMode: 'undoable',
- onSuccess: ({ data }) => {
- notify('Comment approved', { undoable: true });
- redirect('/comments');
- },
- onFailure: (error) => notify(`Error: ${error.message}`, { type: 'warning' }),
- };
- return (
-
- {(approve, { loading }) => (
-
- )}
-
- );
-};
-
-export default ApproveButton;
-```
-
-And here is the `` component using the `withDataProvider` HOC instead of the `useDataProvider` hook:
-
-```diff
-import { useState, useEffect } from 'react';
--import { useDataProvider } from 'react-admin';
-+import { withDataProvider } from 'react-admin';
-
--const UserProfile = ({ userId }) => {
-+const UserProfile = ({ userId, dataProvider }) => {
-- const dataProvider = useDataProvider();
- const [user, setUser] = useState();
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState();
- useEffect(() => {
- dataProvider.getOne('users', { id: userId })
- .then(({ data }) => {
- setUser(data);
- setLoading(false);
- })
- .catch(error => {
- setError(error);
- setLoading(false);
- })
- }, []);
-
- if (loading) return ;
- if (error) return ;
- if (!user) return null;
-
- return (
-
- Name: {user.name}
- Email: {user.email}
-
- )
-};
-
--export default UserProfile;
-+export default withDataProvider(UserProfile);
-```
+The following hooks accept a `mutationMode` option:
-Note that these components are implemented in react-admin using the hooks described earlier. If you're writing new components, prefer the hooks, which are faster, and do not pollute the component tree.
+* [`useUpdate`](#useupdate)
+* [`useUpdateMany`](#useupdatemany)
+* [`useDelete`](#usedelete)
+* [`useDeleteMany`](#usedeletemany)
## Querying The API With `fetch`
-`useQuery`, `useMutation` and `useDataProvider` are "the react-admin way" to query the API, but nothing prevents you from using `fetch` if you want. For instance, when you don't want to add some routing logic to the data provider for an RPC method on your API, that makes perfect sense.
+data Provider method hooks, `useQuery` and `useMutation` are "the react-admin way" to query the API. But nothing prevents you from using `fetch` if you want. For instance, when you don't want to add some routing logic to the data provider for an RPC method on your API, that makes perfect sense.
There is no special react-admin sauce in that case. Here is an example implementation of calling `fetch` in a component:
diff --git a/docs/Caching.md b/docs/Caching.md
index 789e9c133bf..a8f2249f649 100644
--- a/docs/Caching.md
+++ b/docs/Caching.md
@@ -9,22 +9,42 @@ Not hitting the server is the best way to improve a web app performance - and it
## Optimistic Rendering
-By default, react-admin stores all the responses from the dataProvider in the Redux store. This allows displaying the cached result first while fetching for the fresh data. **This behavior is automatic and requires no configuration**.
+By default, react-admin stores all the responses from the dataProvider in a local cache. This allows displaying the cached result first while fetching for the fresh data. This behavior is called **"stale-while-revalidate"**, it is enabled by default and requires no configuration.
-The Redux store is like a local replica of the API, organized by resource, and shared between all the data provider methods of a given resource. That means that if the `getList('posts')` response contains a record of id 123, a call to `getOne('posts', { id: 123 })` will use that record immediately.
+This accelerates the rendering of pages visited multiple times. For instance, if the user visits the detail page for a post twice, here is what react-admin does:
-For instance, if the end-user displays a list of posts, then clicks on a post in the list to display the list details, here is what react-admin does:
+1. Display the empty detail page
+2. Call `dataProvider.getOne('posts', { id: 123 })`, and store the result in local cache
+3. Re-render the detail page with the data from the dataProvider
+4. The user navigates away, then comes back to the post detail page
+5. Render the detail page immediately using the post from the local cache
+6. Call `dataProvider.getOne('posts', { id: 123 })`, and store the result in local cache
+7. If there is a difference with the previous post, re-render the detail with the data from the dataProvider
+
+In addition, as react-admin knows the *vocabulary* of your data provider, it can reuse data from one call to optimize another. This is called **"optimistic rendering"**, and it is also enabled by default. The optimistic rendering uses the semantics of the `dataProvider` verb. That means that requests for a list (`getList`) also populate the cache for individual records (`getOne`, `getMany`). That also means that write requests (`create`, `udpate`, `updateMany`, `delete`, `deleteMany`) invalidate the list cache - because after an update, for instance, the ordering of items can be changed.
+
+For instance, if the end user displays a list of posts, then clicks on a post in the list to display the list details, here is what react-admin does:
1. Display the empty List
-2. Call `dataProvider.getList('posts')`, and store the result in the Redux store
-3. Re-render the List with the data from the Redux store
-4. When the user clicks on a post, display immediately the post from the Redux store
-5. Call `dataProvider.getOne('posts', { id: 123 })`, and store the result in the Redux store
-6. Re-render the detail with the data from the Redux store
+2. Call `dataProvider.getList('posts')`, and store the result in the local cache, both for the list and for each individual post
+3. Re-render the List with the data from the dataProvider
+4. When the user clicks on a post, render the detail page immediately using the post from the local cache
+5. Call `dataProvider.getOne('posts', { id: 123 })`, and store the result in local cache
+6. If there is a difference with the previous post, re-render the detail with the data from the dataProvider
+
+In step 4, react-admin displays the post *before* fetching it, because it's already in the cache from the previous `getList()` call. In most cases, the post from the `getOne()` response is the same as the one from the `getList()` response, so the re-render of step 6 doesn't occur. If the post was modified on the server side between the `getList()` and the `getOne` calls, the end-user will briefly see the outdated version (at step 4), then the up-to-date version (at step 6).
+
+A third optimization used by react-admin is to apply mutations locally before sending them to the dataProvider. This is called **"optimistic updates"**, and it is also enabled by default.
+
+For instance, if a user edits a post, then renders the list, here is what react-admin does:
-In step 4, react-admin displays the post *before* fetching it, because it's already in the Redux store from the previous `getList()` call. In most cases, the post from the `getOne()` response is the same as the one from the `getList()` response, so the re-render of step 6 is invisible to the end-user. If the post was modified on the server side between the `getList()` and the `getOne` calls, the end-user will briefly see the outdated version (at step 4), then the up to date version (at step 6).
+1. Display the post detail page
+2. Upon user submission, update the post that is in the local cache, then call `dataProvider.update('posts', { id: 123, title: 'New title' })`
+3. Re-render the list with the data from the store (without waiting for the dataProvider response).
-Optimistic rendering improves user experience by displaying stale data while getting fresh data from the API, but it does not reduce the ecological footprint of an app, as the web app still makes API requests on every page.
+Optimistic updates allow users to avoid waiting for the server feedback for simple mutations. It works on updates and deletions.
+
+These 3 techniques improve user experience by displaying stale data while getting fresh data from the API. But they do not reduce the ecological footprint of an app, as the web app still makes API requests on every page.
**Tip**: This design choice explains why react-admin requires that all data provider methods return records of the same shape for a given resource. Otherwise, if the posts returned by `getList()` contain fewer fields than the posts returned by `getOne()`, in the previous scenario, the user will see an incomplete post at step 4.
@@ -66,95 +86,32 @@ Finally, if your API uses GraphQL, it probably doesn't offer HTTP caching.
## Application Cache
-React-admin comes with its caching system, called *application cache*, to overcome the limitations if the HTTP cache. **This cache is opt-in** - you have to enable it by including validity information in the `dataProvider` response. But before explaining how to configure it, let's see how it works.
-
-React-admin already stores responses from the `dataProvider` in the Redux store, for the [optimistic rendering](#optimistic-rendering). The application cache checks if this data is valid, and *skips the call to the `dataProvider` altogether* if it's the case.
-
-For instance, if the end-user displays a list of posts, then clicks on a post in the list to display the list details, here is what react-admin does:
-
-1. Display the empty List
-2. Call `dataProvider.getList('posts')`, and store the result in the Redux store
-3. Re-render the List with the data from the Redux store
-4. When the user clicks on a post, display immediately the post from the Redux store (optimistic rendering)
-5. Check the post of id 123 is still valid, and as it's the case, end here
-
-The application cache uses the semantics of the `dataProvider` verb. That means that requests for a list (`getList`) also populate the cache for individual records (`getOne`, `getMany`). That also means that write requests (`create`, `udpate`, `updateMany`, `delete`, `deleteMany`) invalidate the list cache - because after an update, for instance, the ordering of items can be changed.
-
-So the application cache uses expiration caching together with a deeper knowledge of the data model, to allow longer expirations without the risk of displaying stale data. It especially fits admins for API backends with a small number of users (because with a large number of users, there is a high chance that a record kept in the client-side cache for a few minutes may be updated on the backend by another user). It also works with GraphQL APIs.
+React-admin uses react-query for data fetching. React-query comes with its own caching system, allowing you to skip API calls completely. React-admin calls this the *application cache*. It's a good way to overcome the limitations if the HTTP cache. **This cache is opt-in** - you have to enable it by setting a custom `queryClient` in your `` with a specific `staleTime` option.
-To enable it, the `dataProvider` response must include a `validUntil` key, containing the date until which the record(s) is (are) valid.
-
-```diff
-// response to getOne('posts', { id: 123 })
-{
- "data": { "id": 123, "title": "Hello, world" }
-+ "validUntil": new Date('2020-03-02T13:24:05')
-}
-
-// response to getMany('posts', { ids: [123, 124] }
-{
- "data": [
- { "id": 123, "title": "Hello, world" },
- { "id": 124, "title": "Post title 2" },
- ],
-+ "validUntil": new Date('2020-03-02T13:24:05')
-}
-
-// response to getList('posts')
-{
- "data": [
- { "id": 123, "title": "Hello, world" },
- { "id": 124, "title": "Post title 2" },
- ...
-
- ],
- "total": 45,
-+ "validUntil": new Date('2020-03-02T13:24:05')
-}
-```
-
-To empty the cache, the `dataProvider` can simply omit the `validUntil` key in the response.
-
-**Tip**: As of writing, the `validUntil` key is only taken into account for `getOne`, `getMany`, and `getList`.
-
-It's your responsibility to determine the validity date based on the API response, or based on a fixed time policy.
-
-For instance, to have a `dataProvider` declare responses for `getOne`, `getMany`, and `getList` valid for 5 minutes, you can wrap it in the following proxy:
-
-```js
-// in src/dataProvider.js
-import simpleRestProvider from 'ra-data-simple-rest';
-
-const dataProvider = simpleRestProvider('http://path.to.my.api/');
-
-const cacheDataProviderProxy = (dataProvider, duration = 5 * 60 * 1000) =>
- new Proxy(dataProvider, {
- get: (target, name) => (resource, params) => {
- if (name === 'getOne' || name === 'getMany' || name === 'getList') {
- return dataProvider[name](resource, params).then(response => {
- const validUntil = new Date();
- validUntil.setTime(validUntil.getTime() + duration);
- response.validUntil = validUntil;
- return response;
- });
- }
- return dataProvider[name](resource, params);
+```jsx
+import { QueryClient } from 'react-query';
+import { Admin, Resource } from 'react-admin';
+
+const App = () => {
+ const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ staleTime: 5 * 60 * 1000, // 5 minutes
+ },
},
});
-
-export default cacheDataProviderProxy(dataProvider);
+ return (
+
+
+
+ );
+}
```
-**Tip**: As caching responses for a fixed period is a common pattern, react-admin exports this `cacheDataProviderProxy` wrapper, so you can write the following instead:
-
-```jsx
-// in src/dataProvider.js
-import simpleRestProvider from 'ra-data-simple-rest';
-import { cacheDataProviderProxy } from 'react-admin';
+With this setting, all queries will be considered valid for 5 minutes. That means that react-admin *won't refetch* data from the API if the data is already in the cache and younger than 5 minutes.
-const dataProvider = simpleRestProvider('http://path.to.my.api/');
+Check the details about this cache [in the react-query documentation](https://react-query.tanstack.com/guides/caching).
-export default cacheDataProviderProxy(dataProvider);
-```
+It especially fits admins for API backends with a small number of users (because with a large number of users, there is a high chance that a record kept in the client-side cache for a few minutes may be updated on the backend by another user). It also works with GraphQL APIs.
Application cache provides a very significant boost for the end-user and saves a large portion of the network traffic. Even a short expiration date (30 seconds or one minute) can speed up a complex admin with a low risk of displaying stale data. Adding an application cache is, therefore, a warmly recommended practice!
diff --git a/docs/CreateEdit.md b/docs/CreateEdit.md
index 1c20b8ea0c2..3d4a219ab36 100644
--- a/docs/CreateEdit.md
+++ b/docs/CreateEdit.md
@@ -102,7 +102,6 @@ You can customize the `` and `` components using the following pro
* [`mutationMode`](#mutationmode) (`` only)
* [`mutationOptions](#mutationoptions)
* [`transform`](#transform)
-* [`successMessage`](#success-message) (deprecated - use `onSuccess` instead)
`` also accepts a `record` prop, to initialize the form based on a value object.
@@ -467,8 +466,6 @@ const PostEdit = () => {
}
```
-**Tip**: When you set the `onSuccess` prop, the `successMessage` prop is ignored.
-
**Tip**: If you want to have different success side effects based on the button clicked by the user (e.g. if the creation form displays two submit buttons, one to "save and redirect to the list", and another to "save and display an empty form"), you can set the `onSuccess` prop on the `` component, too.
Similarly, you can override the failure side effects with an `onError` otion. By default, when the save action fails at the dataProvider level, react-admin shows an error notification. On an Edit page with `mutationMode` set to `undoable` or `optimistic`, it refreshes the page, too.
@@ -541,20 +538,6 @@ The `transform` function can also return a `Promise`, which allows you to do all
**Tip**: If you want to have different transformations based on the button clicked by the user (e.g. if the creation form displays two submit buttons, one to "save", and another to "save and notify other admins"), you can set the `transform` prop on the `` component, too. See [Altering the Form Values Before Submitting](#altering-the-form-values-before-submitting) for an example.
-### Success message
-
-**Deprecated**: use the `onSuccess` prop instead. See [Changing The Success or Failure Notification Message](#changing-the-success-or-failure-notification-message) for the new syntax.
-
-Once the `dataProvider` returns successfully after save, users see a generic notification ("Element created" / "Element updated"). You can customize this message by passing a `successMessage` prop:
-
-```jsx
-const PostEdit = props => (
-
- // ...
-
-);
-```
-
**Tip**: The message will be translated.
## Prefilling a `` Record
diff --git a/docs/Reference.md b/docs/Reference.md
index 339478471ea..6c90b7efd39 100644
--- a/docs/Reference.md
+++ b/docs/Reference.md
@@ -85,7 +85,6 @@ title: "Reference"
* [``](https://marmelab.com/ra-enterprise/modules/ra-markdown#markdowninput)
* [``](./Theming.md#using-a-custom-menu)
* [``](https://marmelab.com/ra-enterprise/modules/ra-navigation#multilevelmenu-replacing-the-default-menu-by-a-multi-level-one)
-* [``](./Actions.md#legacy-components-query-mutation-and-withdataprovider)
* [``](./Theming.md#notifications)
* [``](./Inputs.md#booleaninput-and-nullablebooleaninput)
* [``](./Fields.md#numberfield)
@@ -93,7 +92,6 @@ title: "Reference"
* [``](./List.md#pagination-pagination-component)
* [``](./Inputs.md#passwordinput)
* [``](https://marmelab.com/ra-enterprise/modules/ra-preferences#preferencessetter-setting-preferences-declaratively)
-* [``](./Actions.md#legacy-components-query-mutation-and-withdataprovider)
* [``](./Inputs.md#radiobuttongroupinput)
* [``](https://marmelab.com/ra-enterprise/modules/ra-realtime#real-time-views-list-edit-show)
* [``](https://marmelab.com/ra-enterprise/modules/ra-realtime#real-time-views-list-edit-show)
@@ -181,13 +179,10 @@ title: "Reference"
* [`useLogout`](./Authentication.md#uselogout-hook)
* `useLogoutIfAccessDenied`
* [`useMediaQuery`](./Theming.md#usemediaquery-hook)
-* [`useMutation`](./Actions.md#usemutation-hook)
* [`useNotify`](./Actions.md#usenotify)
* `usePaginationState`
* [`usePermissions`](./Authentication.md#usepermissions-hook)
* [`usePreferences`](https://marmelab.com/ra-enterprise/modules/ra-preferences#usepreferences-reading-and-writing-user-preferences)
-* [`useQuery`](./Actions.md#usequery-hook)
-* [`useQueryWithStore`](./Actions.md#usequerywithstore-hook)
* [`useRedirect`](./Actions.md#useredirect)
* `useReference`
* `useReferenceArrayFieldController`
@@ -209,7 +204,6 @@ title: "Reference"
* [`useUpdateMany`](./Actions.md#useupdatemany)
* [`useUnselectAll`](./Actions.md#useunselectall)
* [`useWarnWhenUnsavedChanges`](./CreateEdit.md#warning-about-unsaved-changes)
-* `useVersion`
* [`withDataProvider`](./Actions.md#legacy-components-query-mutation-and-withdataprovider)
* [`withTranslate`](./Translation.md#withtranslate-hoc)
* [``](./Authentication.md#usepermissions-hook)
diff --git a/examples/crm/src/deals/DealColumn.tsx b/examples/crm/src/deals/DealColumn.tsx
index acc3b06c559..68da34e50a9 100644
--- a/examples/crm/src/deals/DealColumn.tsx
+++ b/examples/crm/src/deals/DealColumn.tsx
@@ -2,11 +2,11 @@ import * as React from 'react';
import { styled } from '@mui/material/styles';
import { Typography } from '@mui/material';
import { Droppable } from 'react-beautiful-dnd';
-import { Identifier, RecordMap } from 'react-admin';
+import { Identifier } from 'react-admin';
import { DealCard } from './DealCard';
import { stageNames } from './stages';
-import { Deal } from '../types';
+import { RecordMap } from './DealListContent';
const PREFIX = 'DealColumn';
@@ -48,7 +48,7 @@ export const DealColumn = ({
}: {
stage: string;
dealIds: Identifier[];
- data: RecordMap;
+ data: RecordMap;
}) => {
return (
diff --git a/examples/crm/src/deals/DealListContent.tsx b/examples/crm/src/deals/DealListContent.tsx
index 379c3096611..40ccfb54161 100644
--- a/examples/crm/src/deals/DealListContent.tsx
+++ b/examples/crm/src/deals/DealListContent.tsx
@@ -1,12 +1,6 @@
import * as React from 'react';
import { useState, useEffect, useContext } from 'react';
-import {
- useMutation,
- Identifier,
- useListContext,
- RecordMap,
- DataProviderContext,
-} from 'react-admin';
+import { Identifier, useListContext, DataProviderContext } from 'react-admin';
import { Box } from '@mui/material';
import { DragDropContext, OnDragEndResponder } from 'react-beautiful-dnd';
import isEqual from 'lodash/isEqual';
@@ -15,6 +9,11 @@ import { DealColumn } from './DealColumn';
import { stages } from './stages';
import { Deal } from '../types';
+export interface RecordMap {
+ [id: number]: Deal;
+ [id: string]: Deal;
+}
+
interface DealsByColumn {
[stage: string]: Identifier[];
}
@@ -44,20 +43,13 @@ const getDealsByColumn = (data: Deal[]): DealsByColumn => {
return columns;
};
-const indexById = (data: Deal[]): RecordMap =>
+const indexById = (data: Deal[]): RecordMap =>
data.reduce((obj, record) => ({ ...obj, [record.id]: record }), {});
export const DealListContent = () => {
- const {
- data: unorderedDeals,
- isLoading,
- page,
- perPage,
- currentSort,
- filterValues,
- } = useListContext();
+ const { data: unorderedDeals, isLoading, refetch } = useListContext();
- const [data, setData] = useState>(
+ const [data, setData] = useState(
isLoading ? {} : indexById(unorderedDeals)
);
const [deals, setDeals] = useState(
@@ -66,17 +58,6 @@ export const DealListContent = () => {
// we use the raw dataProvider to avoid too many updates to the Redux store after updates (which would create junk)
const dataProvider = useContext(DataProviderContext);
- // FIXME: use refetch when available
- const [refresh] = useMutation({
- resource: 'deals',
- type: 'getList',
- payload: {
- pagination: { page, perPage },
- sort: currentSort,
- filter: filterValues,
- },
- });
-
// update deals by columns when the dataProvider response updates
useEffect(() => {
if (isLoading) return;
@@ -158,7 +139,7 @@ export const DealListContent = () => {
}),
]);
- refresh();
+ refetch();
} else {
// deal moved down, e.g
// src dest
@@ -188,7 +169,7 @@ export const DealListContent = () => {
}),
]);
- refresh();
+ refetch();
}
} else {
// moving deal across columns
@@ -265,7 +246,7 @@ export const DealListContent = () => {
}),
]);
- refresh();
+ refetch();
}
};
diff --git a/examples/demo/src/dashboard/Dashboard.tsx b/examples/demo/src/dashboard/Dashboard.tsx
index 97166777176..157529802f3 100644
--- a/examples/demo/src/dashboard/Dashboard.tsx
+++ b/examples/demo/src/dashboard/Dashboard.tsx
@@ -1,5 +1,5 @@
import React, { useState, useEffect, useCallback, CSSProperties } from 'react';
-import { useVersion, useDataProvider } from 'react-admin';
+import { useDataProvider } from 'react-admin';
import { useMediaQuery, Theme } from '@mui/material';
import { subDays } from 'date-fns';
@@ -47,7 +47,6 @@ const VerticalSpacer = () => ;
const Dashboard = () => {
const [state, setState] = useState({});
- const version = useVersion();
const dataProvider = useDataProvider();
const isXSmall = useMediaQuery((theme: Theme) =>
theme.breakpoints.down('sm')
@@ -150,7 +149,7 @@ const Dashboard = () => {
useEffect(() => {
fetchOrders();
fetchReviews();
- }, [version]); // eslint-disable-line react-hooks/exhaustive-deps
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
const {
nbNewOrders,
diff --git a/examples/demo/src/dashboard/NewCustomers.tsx b/examples/demo/src/dashboard/NewCustomers.tsx
index 37f6ab1344a..1b96649fd3a 100644
--- a/examples/demo/src/dashboard/NewCustomers.tsx
+++ b/examples/demo/src/dashboard/NewCustomers.tsx
@@ -11,7 +11,7 @@ import {
} from '@mui/material';
import CustomerIcon from '@mui/icons-material/PersonAdd';
import { Link } from 'react-router-dom';
-import { useTranslate, useQueryWithStore } from 'react-admin';
+import { useTranslate, useGetList } from 'react-admin';
import { subDays } from 'date-fns';
import CardWithIcon from './CardWithIcon';
@@ -44,20 +44,16 @@ const NewCustomers = () => {
aMonthAgo.setSeconds(0);
aMonthAgo.setMilliseconds(0);
- const { loaded, data: visitors } = useQueryWithStore({
- type: 'getList',
- resource: 'customers',
- payload: {
- filter: {
- has_ordered: true,
- first_seen_gte: aMonthAgo.toISOString(),
- },
- sort: { field: 'first_seen', order: 'DESC' },
- pagination: { page: 1, perPage: 100 },
+ const { isLoading, data: visitors } = useGetList('customers', {
+ filter: {
+ has_ordered: true,
+ first_seen_gte: aMonthAgo.toISOString(),
},
+ sort: { field: 'first_seen', order: 'DESC' },
+ pagination: { page: 1, perPage: 100 },
});
- if (!loaded) return null;
+ if (isLoading) return null;
const nb = visitors ? visitors.reduce((nb: number) => ++nb, 0) : 0;
return (
diff --git a/examples/demo/src/orders/Basket.tsx b/examples/demo/src/orders/Basket.tsx
index 96bc7fde288..0321d23a933 100644
--- a/examples/demo/src/orders/Basket.tsx
+++ b/examples/demo/src/orders/Basket.tsx
@@ -7,9 +7,9 @@ import {
TableHead,
TableRow,
} from '@mui/material';
-import { Link, FieldProps, useTranslate, useQueryWithStore } from 'react-admin';
+import { Link, FieldProps, useTranslate, useGetMany } from 'react-admin';
-import { AppState, Order, Product } from '../types';
+import { Order, Product } from '../types';
const PREFIX = 'Basket';
@@ -26,36 +26,21 @@ const Basket = (props: FieldProps) => {
const translate = useTranslate();
- const { loaded, data: products } = useQueryWithStore(
- {
- type: 'getMany',
- resource: 'products',
- payload: {
- ids: record ? record.basket.map(item => item.product_id) : [],
- },
- },
- {},
- state => {
- const productIds = record
- ? record.basket.map(item => item.product_id)
- : [];
+ const productIds = record ? record.basket.map(item => item.product_id) : [];
- return productIds
- .map(
- productId =>
- state.admin.resources.products.data[
- productId
- ] as Product
- )
- .filter(r => typeof r !== 'undefined')
- .reduce((prev, next) => {
- prev[next.id] = next;
- return prev;
- }, {} as { [key: string]: Product });
- }
+ const { isLoading, data: products } = useGetMany(
+ 'products',
+ { ids: productIds },
+ { enabled: !!record }
);
+ const productsById = products
+ ? products.reduce((acc, product) => {
+ acc[product.id] = product;
+ return acc;
+ }, {} as any)
+ : {};
- if (!loaded || !record) return null;
+ if (isLoading || !record || !products) return null;
return (
@@ -80,38 +65,36 @@ const Basket = (props: FieldProps) => {
- {record.basket.map(
- (item: any) =>
- products[item.product_id] && (
-
-
-
- {products[item.product_id].reference}
-
-
-
- {products[
- item.product_id
- ].price.toLocaleString(undefined, {
- style: 'currency',
- currency: 'USD',
- })}
-
-
- {item.quantity}
-
-
- {(
- products[item.product_id].price *
- item.quantity
- ).toLocaleString(undefined, {
- style: 'currency',
- currency: 'USD',
- })}
-
-
- )
- )}
+ {record.basket.map((item: any) => (
+
+
+
+ {productsById[item.product_id].reference}
+
+
+
+ {productsById[item.product_id].price.toLocaleString(
+ undefined,
+ {
+ style: 'currency',
+ currency: 'USD',
+ }
+ )}
+
+
+ {item.quantity}
+
+
+ {(
+ productsById[item.product_id].price *
+ item.quantity
+ ).toLocaleString(undefined, {
+ style: 'currency',
+ currency: 'USD',
+ })}
+
+
+ ))}
);
diff --git a/examples/demo/src/reviews/ReviewEdit.tsx b/examples/demo/src/reviews/ReviewEdit.tsx
index 5030ff6d550..d2cfa91cfea 100644
--- a/examples/demo/src/reviews/ReviewEdit.tsx
+++ b/examples/demo/src/reviews/ReviewEdit.tsx
@@ -80,7 +80,6 @@ const ReviewEdit = ({ onCancel, ...props }: Props) => {
className={classes.form}
record={controllerProps.record}
save={controllerProps.save}
- version={controllerProps.version}
redirect="list"
resource="reviews"
toolbar={ }
diff --git a/examples/simple/src/comments/CommentEdit.tsx b/examples/simple/src/comments/CommentEdit.tsx
index 911374586cc..86627d99ecb 100644
--- a/examples/simple/src/comments/CommentEdit.tsx
+++ b/examples/simple/src/comments/CommentEdit.tsx
@@ -113,7 +113,7 @@ const CreatePost = () => {
const CommentEdit = props => {
const controllerProps = useEditController(props);
- const { resource, record, redirect, save, version } = controllerProps;
+ const { resource, record, redirect, save } = controllerProps;
return (
@@ -133,7 +133,6 @@ const CommentEdit = props => {
resource={resource}
record={record}
save={save}
- version={version}
warnWhenUnsavedChanges
>
diff --git a/examples/simple/src/comments/PostPreview.tsx b/examples/simple/src/comments/PostPreview.tsx
index 67a26212ce1..71e5d51ef9e 100644
--- a/examples/simple/src/comments/PostPreview.tsx
+++ b/examples/simple/src/comments/PostPreview.tsx
@@ -1,43 +1,35 @@
import * as React from 'react';
-import { useSelector } from 'react-redux';
+import { useQueryClient } from 'react-query';
import {
SimpleShowLayout,
TextField,
- ReduxState,
+ ResourceContextProvider,
Identifier,
Record,
} from 'react-admin';
-const PostPreview = ({
+const PostPreview = ({
id,
- basePath,
resource,
}: {
id: Identifier;
- basePath: string;
resource: string;
}) => {
- const record = useSelector(state =>
- state.admin.resources[resource]
- ? state.admin.resources[resource].data[id]
- : null
- );
- const version = useSelector(
- state => state.admin.ui.viewVersion
- );
- useSelector(state => state.admin.loading > 0);
+ const queryClient = useQueryClient();
+ const record = queryClient.getQueryData([
+ resource,
+ 'getOne',
+ String(id),
+ ]);
return (
-
-
-
-
-
+
+
+
+
+
+
+
);
};
diff --git a/examples/simple/src/comments/PostQuickCreate.tsx b/examples/simple/src/comments/PostQuickCreate.tsx
index 02ddadda3b3..f2d74118100 100644
--- a/examples/simple/src/comments/PostQuickCreate.tsx
+++ b/examples/simple/src/comments/PostQuickCreate.tsx
@@ -2,16 +2,15 @@ import * as React from 'react';
import { styled } from '@mui/material/styles';
import { useCallback } from 'react';
import PropTypes from 'prop-types';
-import { useSelector } from 'react-redux';
import {
SaveButton,
SimpleForm,
TextInput,
Toolbar,
required,
- ReduxState,
useCreate,
useNotify,
+ useLoading,
} from 'react-admin'; // eslint-disable-line import/no-unresolved
import CancelButton from './PostQuickCreateCancelButton';
@@ -43,9 +42,7 @@ PostQuickCreateToolbar.propTypes = {
const PostQuickCreate = ({ onCancel, onSave, ...props }) => {
const [create] = useCreate();
const notify = useNotify();
- const submitting = useSelector(
- state => state.admin.loading > 0
- );
+ const submitting = useLoading();
const handleSave = useCallback(
values => {
diff --git a/examples/simple/src/comments/PostReferenceInput.tsx b/examples/simple/src/comments/PostReferenceInput.tsx
index 6b9b22824d4..61292ef9db5 100644
--- a/examples/simple/src/comments/PostReferenceInput.tsx
+++ b/examples/simple/src/comments/PostReferenceInput.tsx
@@ -38,7 +38,6 @@ const PostReferenceInput = props => {
const [showCreateDialog, setShowCreateDialog] = useState(false);
const [showPreviewDialog, setShowPreviewDialog] = useState(false);
const [newPostId, setNewPostId] = useState('');
- const [version, setVersion] = useState(0);
const handleNewClick = useCallback(
event => {
@@ -70,14 +69,13 @@ const PostReferenceInput = props => {
setNewPostId(post.id);
change('post_id', post.id);
queryClient.invalidateQueries(['posts', 'getList']);
- setVersion(previous => previous + 1);
},
[setShowCreateDialog, setNewPostId, change, queryClient]
);
return (
-
+
{
+ const form = useForm();
+ const notify = useNotify();
+ return (
+ {
+ // FIXME for some reason, form.reset() doesn't work here
+ form.getRegisteredFields().forEach(field => {
+ if (field.includes('[')) {
+ // input inside an array input, skipping
+ return;
+ }
+ form.change(field, form.getState().initialValues[field]);
+ });
+ form.restart();
+ window.scrollTo(0, 0);
+ notify('ra.notification.created', 'info');
+ }}
+ />
+ );
+};
const PostCreateToolbar = props => (
@@ -36,12 +64,7 @@ const PostCreateToolbar = props => (
submitOnEnter={false}
variant="text"
/>
-
+
({ ...data, average_note: 10 })}
diff --git a/packages/ra-core/src/actions/dataActions/crudCreate.ts b/packages/ra-core/src/actions/dataActions/crudCreate.ts
deleted file mode 100644
index 82ac4458944..00000000000
--- a/packages/ra-core/src/actions/dataActions/crudCreate.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import { Record } from '../../types';
-import { CREATE } from '../../core';
-import { FETCH_END, FETCH_ERROR } from '../fetchActions';
-
-export const crudCreate = (resource: string, data: any): CrudCreateAction => ({
- type: CRUD_CREATE,
- payload: { data },
- meta: {
- resource,
- fetch: CREATE,
- },
-});
-
-interface RequestPayload {
- data: any;
-}
-
-export const CRUD_CREATE = 'RA/CRUD_CREATE';
-export interface CrudCreateAction {
- readonly type: typeof CRUD_CREATE;
- readonly payload: RequestPayload;
- readonly meta: {
- resource: string;
- fetch: typeof CREATE;
- };
-}
-
-export const CRUD_CREATE_LOADING = 'RA/CRUD_CREATE_LOADING';
-export interface CrudCreateLoadingAction {
- readonly type: typeof CRUD_CREATE_LOADING;
- readonly payload: RequestPayload;
- readonly meta: {
- resource: string;
- };
-}
-
-export const CRUD_CREATE_FAILURE = 'RA/CRUD_CREATE_FAILURE';
-export interface CrudCreateFailureAction {
- readonly type: typeof CRUD_CREATE_FAILURE;
- readonly error: string | object;
- readonly payload: string;
- readonly requestPayload: RequestPayload;
- readonly meta: {
- resource: string;
- fetchResponse: typeof CREATE;
- fetchStatus: typeof FETCH_ERROR;
- };
-}
-
-export const CRUD_CREATE_SUCCESS = 'RA/CRUD_CREATE_SUCCESS';
-export interface CrudCreateSuccessAction {
- readonly type: typeof CRUD_CREATE_SUCCESS;
- readonly payload: {
- data: Record;
- };
- readonly requestPayload: RequestPayload;
- readonly meta: {
- resource: string;
- fetchResponse: typeof CREATE;
- fetchStatus: typeof FETCH_END;
- };
-}
diff --git a/packages/ra-core/src/actions/dataActions/crudDelete.ts b/packages/ra-core/src/actions/dataActions/crudDelete.ts
deleted file mode 100644
index 93dbbd4b6a0..00000000000
--- a/packages/ra-core/src/actions/dataActions/crudDelete.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-import { Identifier, Record } from '../../types';
-import { DELETE } from '../../core';
-import { FETCH_END, FETCH_ERROR } from '../fetchActions';
-
-export const crudDelete = (
- resource: string,
- id: Identifier,
- previousData: Record
-): CrudDeleteAction => ({
- type: CRUD_DELETE,
- payload: { id, previousData },
- meta: {
- resource,
- fetch: DELETE,
- },
-});
-
-interface RequestPayload {
- id: Identifier;
- previousData: Record;
-}
-
-export const CRUD_DELETE = 'RA/CRUD_DELETE';
-export interface CrudDeleteAction {
- readonly type: typeof CRUD_DELETE;
- readonly payload: RequestPayload;
- readonly meta: {
- resource: string;
- fetch: typeof DELETE;
- };
-}
-
-export const CRUD_DELETE_LOADING = 'RA/CRUD_DELETE_LOADING';
-export interface CrudDeleteLoadingAction {
- readonly type: typeof CRUD_DELETE_LOADING;
- readonly payload: RequestPayload;
- readonly meta: {
- resource: string;
- };
-}
-
-export const CRUD_DELETE_FAILURE = 'RA/CRUD_DELETE_FAILURE';
-export interface CrudDeleteFailureAction {
- readonly type: typeof CRUD_DELETE_FAILURE;
- readonly error: string | object;
- readonly payload: string;
- readonly requestPayload: RequestPayload;
- readonly meta: {
- resource: string;
- fetchResponse: typeof CRUD_DELETE;
- fetchStatus: typeof FETCH_ERROR;
- };
-}
-
-export const CRUD_DELETE_SUCCESS = 'RA/CRUD_DELETE_SUCCESS';
-export interface CrudDeleteSuccessAction {
- readonly type: typeof CRUD_DELETE_SUCCESS;
- readonly payload: {
- data: Record;
- };
- readonly requestPayload: RequestPayload;
- readonly meta: {
- resource: string;
- fetchResponse: typeof CRUD_DELETE;
- fetchStatus: typeof FETCH_END;
- };
-}
diff --git a/packages/ra-core/src/actions/dataActions/crudDeleteMany.ts b/packages/ra-core/src/actions/dataActions/crudDeleteMany.ts
deleted file mode 100644
index a0b69151cd4..00000000000
--- a/packages/ra-core/src/actions/dataActions/crudDeleteMany.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import { Identifier, Record } from '../../types';
-import { DELETE_MANY } from '../../core';
-import { FETCH_END, FETCH_ERROR } from '../fetchActions';
-
-export const crudDeleteMany = (
- resource: string,
- ids: Identifier[]
-): CrudDeleteManyAction => ({
- type: CRUD_DELETE_MANY,
- payload: { ids },
- meta: {
- resource,
- fetch: DELETE_MANY,
- },
-});
-
-interface RequestPayload {
- ids: Identifier[];
-}
-
-export const CRUD_DELETE_MANY = 'RA/CRUD_DELETE_MANY';
-export interface CrudDeleteManyAction {
- readonly type: typeof CRUD_DELETE_MANY;
- readonly payload: RequestPayload;
- readonly meta: {
- resource: string;
- fetch: typeof DELETE_MANY;
- };
-}
-
-export const CRUD_DELETE_MANY_LOADING = 'RA/CRUD_DELETE_MANY_LOADING';
-export interface CrudDeleteManyLoadingAction {
- readonly type: typeof CRUD_DELETE_MANY_LOADING;
- readonly payload: RequestPayload;
- readonly meta: {
- resource: string;
- };
-}
-
-export const CRUD_DELETE_MANY_FAILURE = 'RA/CRUD_DELETE_MANY_FAILURE';
-export interface CrudDeleteMAnyFailureAction {
- readonly type: typeof CRUD_DELETE_MANY_FAILURE;
- readonly error: string | object;
- readonly payload: string;
- readonly requestPayload: RequestPayload;
- readonly meta: {
- resource: string;
- fetchResponse: typeof DELETE_MANY;
- fetchStatus: typeof FETCH_ERROR;
- };
-}
-
-export const CRUD_DELETE_MANY_SUCCESS = 'RA/CRUD_DELETE_MANY_SUCCESS';
-export interface CrudDeleteManySuccessAction {
- readonly type: typeof CRUD_DELETE_MANY_SUCCESS;
- readonly payload: {
- data: Record[];
- };
- readonly requestPayload: RequestPayload;
- readonly meta: {
- resource: string;
- fetchResponse: typeof DELETE_MANY;
- fetchStatus: typeof FETCH_END;
- };
-}
diff --git a/packages/ra-core/src/actions/dataActions/crudGetAll.ts b/packages/ra-core/src/actions/dataActions/crudGetAll.ts
deleted file mode 100644
index 4041756ca97..00000000000
--- a/packages/ra-core/src/actions/dataActions/crudGetAll.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-import { Record, PaginationPayload, SortPayload } from '../../types';
-import { GET_LIST } from '../../core';
-import { FETCH_END, FETCH_ERROR } from '../fetchActions';
-
-export const crudGetAll = (
- resource: string,
- sort: SortPayload,
- filter: object,
- maxResults: number
-): CrudGetAllAction => ({
- type: CRUD_GET_ALL,
- payload: { sort, filter, pagination: { page: 1, perPage: maxResults } },
- meta: {
- resource,
- fetch: GET_LIST,
- },
-});
-
-interface RequestPayload {
- pagination: PaginationPayload;
- sort: SortPayload;
- filter: object;
-}
-
-export const CRUD_GET_ALL = 'RA/CRUD_GET_ALL';
-interface CrudGetAllAction {
- readonly type: typeof CRUD_GET_ALL;
- readonly payload: RequestPayload;
- readonly meta: {
- resource: string;
- fetch: typeof GET_LIST;
- };
-}
-
-export const CRUD_GET_ALL_LOADING = 'RA/CRUD_GET_ALL_LOADING';
-export interface CrudGetAllLoadingAction {
- readonly type: typeof CRUD_GET_ALL_LOADING;
- readonly payload: RequestPayload;
- readonly meta: {
- resource: string;
- };
-}
-
-export const CRUD_GET_ALL_FAILURE = 'RA/CRUD_GET_ALL_FAILURE';
-export interface CrudGetAllFailureAction {
- readonly type: typeof CRUD_GET_ALL_FAILURE;
- readonly error: string | object;
- readonly payload: string;
- readonly requestPayload: RequestPayload;
- readonly meta: {
- resource: string;
- fetchResponse: typeof GET_LIST;
- fetchStatus: typeof FETCH_ERROR;
- };
-}
-
-export const CRUD_GET_ALL_SUCCESS = 'RA/CRUD_GET_ALL_SUCCESS';
-export interface CrudGetAllSuccessAction {
- readonly type: typeof CRUD_GET_ALL_SUCCESS;
- readonly payload: {
- data: Record[];
- total: number;
- };
- readonly requestPayload: RequestPayload;
- readonly meta: {
- resource: string;
- fetchResponse: typeof GET_LIST;
- fetchStatus: typeof FETCH_END;
- };
-}
diff --git a/packages/ra-core/src/actions/dataActions/crudGetList.ts b/packages/ra-core/src/actions/dataActions/crudGetList.ts
deleted file mode 100644
index fd47840c22a..00000000000
--- a/packages/ra-core/src/actions/dataActions/crudGetList.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-import { Record, PaginationPayload, SortPayload } from '../../types';
-import { GET_LIST } from '../../core';
-import { FETCH_END, FETCH_ERROR } from '../fetchActions';
-
-export const crudGetList = (
- resource: string,
- pagination: PaginationPayload,
- sort: SortPayload,
- filter: object
-): CrudGetListAction => ({
- type: CRUD_GET_LIST,
- payload: { pagination, sort, filter },
- meta: {
- resource,
- fetch: GET_LIST,
- },
-});
-
-interface RequestPayload {
- pagination: PaginationPayload;
- sort: SortPayload;
- filter: object;
-}
-
-export const CRUD_GET_LIST = 'RA/CRUD_GET_LIST';
-export interface CrudGetListAction {
- readonly type: typeof CRUD_GET_LIST;
- readonly payload: RequestPayload;
- readonly meta: {
- resource: string;
- fetch: typeof GET_LIST;
- };
-}
-
-export const CRUD_GET_LIST_LOADING = 'RA/CRUD_GET_LIST_LOADING';
-export interface CrudGetListLoadingAction {
- readonly type: typeof CRUD_GET_LIST_LOADING;
- readonly payload: RequestPayload;
- readonly meta: {
- resource: string;
- };
-}
-
-export const CRUD_GET_LIST_FAILURE = 'RA/CRUD_GET_LIST_FAILURE';
-export interface CrudGetListFailureAction {
- readonly type: typeof CRUD_GET_LIST_FAILURE;
- readonly error: string | object;
- readonly payload: string;
- readonly requestPayload: RequestPayload;
- readonly meta: {
- resource: string;
- fetchResponse: typeof GET_LIST;
- fetchStatus: typeof FETCH_ERROR;
- };
-}
-
-export const CRUD_GET_LIST_SUCCESS = 'RA/CRUD_GET_LIST_SUCCESS';
-export interface CrudGetListSuccessAction {
- readonly type: typeof CRUD_GET_LIST_SUCCESS;
- readonly payload: {
- data: Record[];
- total: number;
- };
- readonly requestPayload: RequestPayload;
- readonly meta: {
- resource: string;
- fetchResponse: typeof GET_LIST;
- fetchStatus: typeof FETCH_END;
- };
-}
diff --git a/packages/ra-core/src/actions/dataActions/crudGetMany.ts b/packages/ra-core/src/actions/dataActions/crudGetMany.ts
deleted file mode 100644
index 5e36c06e1c7..00000000000
--- a/packages/ra-core/src/actions/dataActions/crudGetMany.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import { Identifier, Record } from '../../types';
-import { GET_MANY } from '../../core';
-import { FETCH_END, FETCH_ERROR } from '../fetchActions';
-
-export const crudGetMany = (
- resource: string,
- ids: Identifier[]
-): CrudGetManyAction => ({
- type: CRUD_GET_MANY,
- payload: { ids },
- meta: {
- resource,
- fetch: GET_MANY,
- },
-});
-
-interface RequestPayload {
- ids: Identifier[];
-}
-
-export const CRUD_GET_MANY = 'RA/CRUD_GET_MANY';
-export interface CrudGetManyAction {
- readonly type: typeof CRUD_GET_MANY;
- readonly payload: RequestPayload;
- readonly meta: {
- resource: string;
- fetch: typeof GET_MANY;
- };
-}
-
-export const CRUD_GET_MANY_LOADING = 'RA/CRUD_GET_MANY_LOADING';
-export interface CrudGetManyLoadingAction {
- readonly type: typeof CRUD_GET_MANY_LOADING;
- readonly payload: RequestPayload;
- readonly meta: {
- resource: string;
- };
-}
-
-export const CRUD_GET_MANY_FAILURE = 'RA/CRUD_GET_MANY_FAILURE';
-export interface CrudGetManyFailureAction {
- readonly type: typeof CRUD_GET_MANY_FAILURE;
- readonly error: string | object;
- readonly payload: string;
- readonly requestPayload: RequestPayload;
- readonly meta: {
- resource: string;
- fetchResponse: typeof GET_MANY;
- fetchStatus: typeof FETCH_ERROR;
- };
-}
-
-export const CRUD_GET_MANY_SUCCESS = 'RA/CRUD_GET_MANY_SUCCESS';
-export interface CrudGetManySuccessAction {
- readonly type: typeof CRUD_GET_MANY_SUCCESS;
- readonly payload: {
- data: Record[];
- };
- readonly requestPayload: RequestPayload;
- readonly meta: {
- resource: string;
- fetchResponse: typeof GET_MANY;
- fetchStatus: typeof FETCH_END;
- };
-}
diff --git a/packages/ra-core/src/actions/dataActions/crudGetManyReference.ts b/packages/ra-core/src/actions/dataActions/crudGetManyReference.ts
deleted file mode 100644
index d713329f8ff..00000000000
--- a/packages/ra-core/src/actions/dataActions/crudGetManyReference.ts
+++ /dev/null
@@ -1,90 +0,0 @@
-import {
- Identifier,
- Record,
- PaginationPayload,
- SortPayload,
-} from '../../types';
-import { GET_MANY_REFERENCE } from '../../core';
-import { FETCH_END, FETCH_ERROR } from '../fetchActions';
-
-export const crudGetManyReference = (
- reference: string,
- target: string,
- id: Identifier,
- relatedTo: string,
- pagination: PaginationPayload,
- sort: SortPayload,
- filter: object,
- source: string
-): CrudGetManyReferenceAction => ({
- type: CRUD_GET_MANY_REFERENCE,
- payload: { target, id, pagination, sort, filter, source },
- meta: {
- resource: reference,
- relatedTo,
- fetch: GET_MANY_REFERENCE,
- },
-});
-
-interface RequestPayload {
- source: string;
- target: string;
- id: Identifier;
- pagination: PaginationPayload;
- sort: SortPayload;
- filter: object;
-}
-
-export const CRUD_GET_MANY_REFERENCE = 'RA/CRUD_GET_MANY_REFERENCE';
-export interface CrudGetManyReferenceAction {
- readonly type: typeof CRUD_GET_MANY_REFERENCE;
- readonly payload: RequestPayload;
- readonly meta: {
- resource: string;
- fetch: typeof GET_MANY_REFERENCE;
- relatedTo: string;
- };
-}
-
-export const CRUD_GET_MANY_REFERENCE_LOADING =
- 'RA/CRUD_GET_MANY_REFERENCE_LOADING';
-export interface CrudGetManyReferenceLoadingAction {
- readonly type: typeof CRUD_GET_MANY_REFERENCE_LOADING;
- readonly payload: RequestPayload;
- readonly meta: {
- resource: string;
- relatedTo: string;
- };
-}
-
-export const CRUD_GET_MANY_REFERENCE_FAILURE =
- 'RA/CRUD_GET_MANY_REFERENCE_FAILURE';
-export interface CrudGetManyReferenceFailureAction {
- readonly type: typeof CRUD_GET_MANY_REFERENCE_FAILURE;
- readonly error: string | object;
- readonly payload: string;
- readonly requestPayload: RequestPayload;
- readonly meta: {
- resource: string;
- relatedTo: string;
- fetchResponse: typeof GET_MANY_REFERENCE;
- fetchStatus: typeof FETCH_ERROR;
- };
-}
-
-export const CRUD_GET_MANY_REFERENCE_SUCCESS =
- 'RA/CRUD_GET_MANY_REFERENCE_SUCCESS';
-export interface CrudGetManyReferenceSuccessAction {
- readonly type: typeof CRUD_GET_MANY_REFERENCE_SUCCESS;
- readonly payload: {
- data: Record[];
- total: number;
- };
- readonly requestPayload: RequestPayload;
- readonly meta: {
- resource: string;
- relatedTo: string;
- fetchResponse: typeof GET_MANY_REFERENCE;
- fetchStatus: typeof FETCH_END;
- };
-}
diff --git a/packages/ra-core/src/actions/dataActions/crudGetMatching.ts b/packages/ra-core/src/actions/dataActions/crudGetMatching.ts
deleted file mode 100644
index b0ddb8a2c6f..00000000000
--- a/packages/ra-core/src/actions/dataActions/crudGetMatching.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-import { Record, PaginationPayload, SortPayload } from '../../types';
-import { GET_LIST } from '../../core';
-import { FETCH_END, FETCH_ERROR } from '../fetchActions';
-
-export const crudGetMatching = (
- reference: string,
- relatedTo: string,
- pagination: PaginationPayload,
- sort: SortPayload,
- filter: object
-): CrudGetMatchingAction => ({
- type: CRUD_GET_MATCHING,
- payload: { pagination, sort, filter },
- meta: {
- resource: reference,
- relatedTo,
- fetch: GET_LIST,
- },
-});
-
-interface RequestPayload {
- pagination: PaginationPayload;
- sort: SortPayload;
- filter: object;
-}
-
-export const CRUD_GET_MATCHING = 'RA/CRUD_GET_MATCHING';
-export interface CrudGetMatchingAction {
- readonly type: typeof CRUD_GET_MATCHING;
- readonly payload: RequestPayload;
- readonly meta: {
- resource: string;
- fetch: typeof GET_LIST;
- relatedTo: string;
- };
-}
-
-export const CRUD_GET_MATCHING_LOADING = 'RA/CRUD_GET_MATCHING_LOADING';
-export interface CrudGetMatchingLoadingAction {
- readonly type: typeof CRUD_GET_MATCHING_LOADING;
- readonly payload: RequestPayload;
- readonly meta: {
- resource: string;
- relatedTo: string;
- };
-}
-
-export const CRUD_GET_MATCHING_FAILURE = 'RA/CRUD_GET_MATCHING_FAILURE';
-export interface CrudGetMatchingFailureAction {
- readonly type: typeof CRUD_GET_MATCHING_FAILURE;
- readonly error: string | object;
- readonly payload: string;
- readonly requestPayload: RequestPayload;
- readonly meta: {
- resource: string;
- relatedTo: string;
- fetchResponse: typeof GET_LIST;
- fetchStatus: typeof FETCH_ERROR;
- };
-}
-
-export const CRUD_GET_MATCHING_SUCCESS = 'RA/CRUD_GET_MATCHING_SUCCESS';
-export interface CrudGetMatchingSuccessAction {
- readonly type: typeof CRUD_GET_MATCHING_SUCCESS;
- readonly payload: {
- data: Record[];
- total: number;
- };
- readonly requestPayload: RequestPayload;
- readonly meta: {
- resource: string;
- relatedTo: string;
- fetchResponse: typeof GET_LIST;
- fetchStatus: typeof FETCH_END;
- };
-}
diff --git a/packages/ra-core/src/actions/dataActions/crudGetOne.ts b/packages/ra-core/src/actions/dataActions/crudGetOne.ts
deleted file mode 100644
index 07c4eb77b65..00000000000
--- a/packages/ra-core/src/actions/dataActions/crudGetOne.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import { Identifier, Record } from '../../types';
-import { GET_ONE } from '../../core';
-import { FETCH_END, FETCH_ERROR } from '../fetchActions';
-
-export const crudGetOne = (
- resource: string,
- id: Identifier
-): CrudGetOneAction => ({
- type: CRUD_GET_ONE,
- payload: { id },
- meta: {
- resource,
- fetch: GET_ONE,
- },
-});
-
-interface RequestPayload {
- id: Identifier;
-}
-
-export const CRUD_GET_ONE = 'RA/CRUD_GET_ONE';
-export interface CrudGetOneAction {
- readonly type: typeof CRUD_GET_ONE;
- readonly payload: RequestPayload;
- readonly meta: {
- resource: string;
- fetch: typeof GET_ONE;
- };
-}
-
-export const CRUD_GET_ONE_LOADING = 'RA/CRUD_GET_ONE_LOADING';
-export interface CrudGetOneLoadingAction {
- readonly type: typeof CRUD_GET_ONE_LOADING;
- readonly payload: RequestPayload;
- readonly meta: {
- resource: string;
- };
-}
-
-export const CRUD_GET_ONE_FAILURE = 'RA/CRUD_GET_ONE_FAILURE';
-export interface CrudGetOneFailureAction {
- readonly type: typeof CRUD_GET_ONE_FAILURE;
- readonly error: string | object;
- readonly payload: string;
- readonly requestPayload: RequestPayload;
- readonly meta: {
- resource: string;
- fetchResponse: typeof GET_ONE;
- fetchStatus: typeof FETCH_ERROR;
- };
-}
-
-export const CRUD_GET_ONE_SUCCESS = 'RA/CRUD_GET_ONE_SUCCESS';
-export interface CrudGetOneSuccessAction {
- readonly type: typeof CRUD_GET_ONE_SUCCESS;
- readonly payload: {
- data: Record;
- };
- readonly requestPayload: RequestPayload;
- readonly meta: {
- resource: string;
- fetchResponse: typeof GET_ONE;
- fetchStatus: typeof FETCH_END;
- };
-}
diff --git a/packages/ra-core/src/actions/dataActions/crudUpdate.ts b/packages/ra-core/src/actions/dataActions/crudUpdate.ts
deleted file mode 100644
index eae94061e3d..00000000000
--- a/packages/ra-core/src/actions/dataActions/crudUpdate.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-import { Identifier, Record } from '../../types';
-import { UPDATE } from '../../core';
-import { FETCH_END, FETCH_ERROR } from '../fetchActions';
-
-export const crudUpdate = (
- resource: string,
- id: Identifier,
- data: any,
- previousData: any
-): CrudUpdateAction => ({
- type: CRUD_UPDATE,
- payload: { id, data, previousData },
- meta: {
- resource,
- fetch: UPDATE,
- },
-});
-
-interface RequestPayload {
- id: Identifier;
- data: any;
- previousData?: any;
-}
-
-export const CRUD_UPDATE = 'RA/CRUD_UPDATE';
-export interface CrudUpdateAction {
- readonly type: typeof CRUD_UPDATE;
- readonly payload: RequestPayload;
- readonly meta: {
- resource: string;
- fetch: typeof UPDATE;
- };
-}
-
-export const CRUD_UPDATE_LOADING = 'RA/CRUD_UPDATE_LOADING';
-export interface CrudUpdateLoadingAction {
- readonly type: typeof CRUD_UPDATE_LOADING;
- readonly payload: RequestPayload;
- readonly meta: {
- resource: string;
- };
-}
-
-export const CRUD_UPDATE_FAILURE = 'RA/CRUD_UPDATE_FAILURE';
-export interface CrudUpdateFailureAction {
- readonly type: typeof CRUD_UPDATE_FAILURE;
- readonly error: string | object;
- readonly payload: string;
- readonly requestPayload: RequestPayload;
- readonly meta: {
- resource: string;
- fetchResponse: typeof UPDATE;
- fetchStatus: typeof FETCH_ERROR;
- };
-}
-
-export const CRUD_UPDATE_SUCCESS = 'RA/CRUD_UPDATE_SUCCESS';
-export interface CrudUpdateSuccessAction {
- readonly type: typeof CRUD_UPDATE_SUCCESS;
- readonly payload: {
- data: Record;
- };
- readonly requestPayload: RequestPayload;
- readonly meta: {
- resource: string;
- fetchResponse: typeof UPDATE;
- fetchStatus: typeof FETCH_END;
- };
-}
diff --git a/packages/ra-core/src/actions/dataActions/crudUpdateMany.ts b/packages/ra-core/src/actions/dataActions/crudUpdateMany.ts
deleted file mode 100644
index 6049fdf28b7..00000000000
--- a/packages/ra-core/src/actions/dataActions/crudUpdateMany.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-import { Identifier } from '../../types';
-import { UPDATE_MANY } from '../../core';
-import { FETCH_END, FETCH_ERROR } from '../fetchActions';
-
-export const crudUpdateMany = (
- resource: string,
- ids: Identifier[],
- data: any
-): CrudUpdateManyAction => ({
- type: CRUD_UPDATE_MANY,
- payload: { ids, data },
- meta: {
- resource,
- fetch: UPDATE_MANY,
- },
-});
-
-interface RequestPayload {
- ids: Identifier[];
- data: any;
-}
-
-export const CRUD_UPDATE_MANY = 'RA/CRUD_UPDATE_MANY';
-export interface CrudUpdateManyAction {
- readonly type: typeof CRUD_UPDATE_MANY;
- readonly payload: RequestPayload;
- readonly meta: {
- resource: string;
- fetch: typeof UPDATE_MANY;
- };
-}
-
-export const CRUD_UPDATE_MANY_LOADING = 'RA/CRUD_UPDATE_MANY_LOADING';
-export interface CrudUpdateManyLoadingAction {
- readonly type: typeof CRUD_UPDATE_MANY_LOADING;
- readonly payload: RequestPayload;
- readonly meta: {
- resource: string;
- };
-}
-
-export const CRUD_UPDATE_MANY_FAILURE = 'RA/CRUD_UPDATE_MANY_FAILURE';
-export interface CrudUpdateManyFailureAction {
- readonly type: typeof CRUD_UPDATE_MANY_FAILURE;
- readonly error: string | object;
- readonly payload: string;
- readonly requestPayload: RequestPayload;
- readonly meta: {
- resource: string;
- fetchResponse: typeof UPDATE_MANY;
- fetchStatus: typeof FETCH_ERROR;
- };
-}
-
-export const CRUD_UPDATE_MANY_SUCCESS = 'RA/CRUD_UPDATE_MANY_SUCCESS';
-export interface CrudUpdateManySuccessAction {
- readonly type: typeof CRUD_UPDATE_MANY_SUCCESS;
- readonly payload: {
- data: Identifier[];
- };
- readonly requestPayload: RequestPayload;
- readonly meta: {
- resource: string;
- fetchResponse: typeof UPDATE_MANY;
- fetchStatus: typeof FETCH_END;
- };
-}
diff --git a/packages/ra-core/src/actions/dataActions/index.ts b/packages/ra-core/src/actions/dataActions/index.ts
deleted file mode 100644
index 22c8f7cb1d6..00000000000
--- a/packages/ra-core/src/actions/dataActions/index.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-export * from './crudCreate';
-export * from './crudDelete';
-export * from './crudDeleteMany';
-export * from './crudGetAll';
-export * from './crudGetList';
-export * from './crudGetMany';
-export * from './crudGetManyReference';
-export * from './crudGetMatching';
-export * from './crudGetOne';
-export * from './crudUpdate';
-export * from './crudUpdateMany';
diff --git a/packages/ra-core/src/actions/fetchActions.ts b/packages/ra-core/src/actions/fetchActions.ts
deleted file mode 100644
index e22cb93c8b9..00000000000
--- a/packages/ra-core/src/actions/fetchActions.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-export const FETCH_START = 'RA/FETCH_START';
-
-export interface FetchStartAction {
- readonly type: typeof FETCH_START;
-}
-
-export const fetchStart = (): FetchStartAction => ({ type: FETCH_START });
-
-export const FETCH_END = 'RA/FETCH_END';
-
-export interface FetchEndAction {
- readonly type: typeof FETCH_END;
-}
-
-export const fetchEnd = (): FetchEndAction => ({ type: FETCH_END });
-
-export const FETCH_ERROR = 'RA/FETCH_ERROR';
-
-export interface FetchErrorAction {
- readonly type: typeof FETCH_ERROR;
-}
-
-export const fetchError = (): FetchErrorAction => ({ type: FETCH_ERROR });
-
-export const FETCH_CANCEL = 'RA/FETCH_CANCEL';
-
-export interface FetchCancelAction {
- readonly type: typeof FETCH_CANCEL;
-}
-
-export const fetchCancel = (): FetchCancelAction => ({ type: FETCH_CANCEL });
diff --git a/packages/ra-core/src/actions/index.ts b/packages/ra-core/src/actions/index.ts
index 45bba9e28dd..504c6f9b2db 100644
--- a/packages/ra-core/src/actions/index.ts
+++ b/packages/ra-core/src/actions/index.ts
@@ -1,6 +1,4 @@
export * from './clearActions';
-export * from './dataActions';
-export * from './fetchActions';
export * from './filterActions';
export * from './listActions';
export * from './localeActions';
diff --git a/packages/ra-core/src/actions/uiActions.ts b/packages/ra-core/src/actions/uiActions.ts
index 3868f19ecc5..aa254206c05 100644
--- a/packages/ra-core/src/actions/uiActions.ts
+++ b/packages/ra-core/src/actions/uiActions.ts
@@ -21,27 +21,3 @@ export const setSidebarVisibility = (
type: SET_SIDEBAR_VISIBILITY,
payload: isOpen,
});
-
-export const REFRESH_VIEW = 'RA/REFRESH_VIEW';
-
-export interface RefreshViewAction {
- readonly type: typeof REFRESH_VIEW;
- readonly payload: { hard: boolean };
-}
-
-export const refreshView = (hard?: boolean): RefreshViewAction => ({
- type: REFRESH_VIEW,
- payload: { hard },
-});
-
-export const SET_AUTOMATIC_REFRESH = 'RA/SET_AUTOMATIC_REFRESH';
-
-export interface SetAutomaticRefreshAction {
- readonly type: typeof SET_AUTOMATIC_REFRESH;
- readonly payload: boolean;
-}
-
-export const setAutomaticRefresh = (enabled: boolean) => ({
- type: SET_AUTOMATIC_REFRESH,
- payload: enabled,
-});
diff --git a/packages/ra-core/src/actions/undoActions.ts b/packages/ra-core/src/actions/undoActions.ts
index fd6da504c2e..4e031bbe38d 100644
--- a/packages/ra-core/src/actions/undoActions.ts
+++ b/packages/ra-core/src/actions/undoActions.ts
@@ -29,23 +29,3 @@ export interface CompleteAction {
export const complete = (): CompleteAction => ({
type: COMPLETE,
});
-
-export const START_OPTIMISTIC_MODE = 'RA/START_OPTIMISTIC_MODE';
-
-export interface StartOptimisticModeAction {
- readonly type: typeof START_OPTIMISTIC_MODE;
-}
-
-export const startOptimisticMode = (): StartOptimisticModeAction => ({
- type: START_OPTIMISTIC_MODE,
-});
-
-export const STOP_OPTIMISTIC_MODE = 'RA/STOP_OPTIMISTIC_MODE';
-
-export interface StopOptimisticModeAction {
- readonly type: typeof STOP_OPTIMISTIC_MODE;
-}
-
-export const stopOptimisticMode = (): StopOptimisticModeAction => ({
- type: STOP_OPTIMISTIC_MODE,
-});
diff --git a/packages/ra-core/src/controller/button/useDeleteWithConfirmController.tsx b/packages/ra-core/src/controller/button/useDeleteWithConfirmController.tsx
index f722d75b38d..845f9bbb055 100644
--- a/packages/ra-core/src/controller/button/useDeleteWithConfirmController.tsx
+++ b/packages/ra-core/src/controller/button/useDeleteWithConfirmController.tsx
@@ -8,7 +8,6 @@ import { UseMutationOptions } from 'react-query';
import { useDelete } from '../../dataProvider';
import {
- useRefresh,
useNotify,
useRedirect,
useUnselect,
@@ -85,7 +84,6 @@ const useDeleteWithConfirmController = (
const notify = useNotify();
const unselect = useUnselect();
const redirect = useRedirect();
- const refresh = useRefresh();
const [deleteOne, { isLoading }] = useDelete();
const handleDialogOpen = e => {
@@ -114,7 +112,6 @@ const useDeleteWithConfirmController = (
});
unselect(resource, [record.id]);
redirect(redirectTo, basePath || `/${resource}`);
- refresh();
},
onError: (error: Error) => {
setOpen(false);
@@ -135,7 +132,6 @@ const useDeleteWithConfirmController = (
},
}
);
- refresh();
},
mutationMode,
...mutationOptions,
@@ -155,7 +151,6 @@ const useDeleteWithConfirmController = (
record,
redirect,
redirectTo,
- refresh,
resource,
unselect,
]
diff --git a/packages/ra-core/src/controller/button/useDeleteWithUndoController.tsx b/packages/ra-core/src/controller/button/useDeleteWithUndoController.tsx
index fac91708ce2..6e68cc312b0 100644
--- a/packages/ra-core/src/controller/button/useDeleteWithUndoController.tsx
+++ b/packages/ra-core/src/controller/button/useDeleteWithUndoController.tsx
@@ -3,7 +3,6 @@ import { UseMutationOptions } from 'react-query';
import { useDelete } from '../../dataProvider';
import {
- useRefresh,
useNotify,
useRedirect,
useUnselect,
@@ -63,7 +62,6 @@ const useDeleteWithUndoController = (
const notify = useNotify();
const unselect = useUnselect();
const redirect = useRedirect();
- const refresh = useRefresh();
const [deleteOne, { isLoading }] = useDelete();
const handleDelete = useCallback(
@@ -81,7 +79,6 @@ const useDeleteWithUndoController = (
});
unselect(resource, [record.id]);
redirect(redirectTo, basePath || `/${resource}`);
- refresh();
},
onError: (error: Error) => {
notify(
@@ -100,7 +97,6 @@ const useDeleteWithUndoController = (
},
}
);
- refresh();
},
mutationMode: 'undoable',
...mutationOptions,
@@ -119,7 +115,6 @@ const useDeleteWithUndoController = (
record,
redirect,
redirectTo,
- refresh,
resource,
unselect,
]
diff --git a/packages/ra-core/src/controller/create/CreateContext.tsx b/packages/ra-core/src/controller/create/CreateContext.tsx
index d6169d68789..7845870fbcd 100644
--- a/packages/ra-core/src/controller/create/CreateContext.tsx
+++ b/packages/ra-core/src/controller/create/CreateContext.tsx
@@ -34,8 +34,6 @@ export const CreateContext = createContext({
resource: null,
save: null,
saving: null,
- successMessage: null,
- version: null,
});
CreateContext.displayName = 'CreateContext';
diff --git a/packages/ra-core/src/controller/create/useCreateContext.tsx b/packages/ra-core/src/controller/create/useCreateContext.tsx
index 74a109a28ed..260287f5204 100644
--- a/packages/ra-core/src/controller/create/useCreateContext.tsx
+++ b/packages/ra-core/src/controller/create/useCreateContext.tsx
@@ -66,8 +66,6 @@ const extractCreateContextProps = ({
resource,
save,
saving,
- successMessage,
- version,
}: any) => ({
record,
defaultTitle,
@@ -83,6 +81,4 @@ const extractCreateContextProps = ({
resource,
save,
saving,
- successMessage,
- version,
});
diff --git a/packages/ra-core/src/controller/create/useCreateController.ts b/packages/ra-core/src/controller/create/useCreateController.ts
index b05d4064e77..7de8fe4c86e 100644
--- a/packages/ra-core/src/controller/create/useCreateController.ts
+++ b/packages/ra-core/src/controller/create/useCreateController.ts
@@ -20,7 +20,6 @@ import {
useSaveModifiers,
} from '../saveModifiers';
import { useTranslate } from '../../i18n';
-import useVersion from '../useVersion';
import { Record, OnSuccess, OnFailure, CreateParams } from '../../types';
import {
useResourceContext,
@@ -53,7 +52,6 @@ export const useCreateController = <
const {
disableAuthentication,
record,
- successMessage,
transform,
mutationOptions = {},
} = props;
@@ -67,15 +65,8 @@ export const useCreateController = <
const redirect = useRedirect();
const recordToUse =
record ?? getRecordFromLocation(location) ?? emptyRecord;
- const version = useVersion();
const { onSuccess, onError, ...otherMutationOptions } = mutationOptions;
- if (process.env.NODE_ENV !== 'production' && successMessage) {
- console.log(
- ' prop is deprecated, use the onSuccess prop instead.'
- );
- }
-
const {
onSuccessRef,
setOnSuccess,
@@ -117,14 +108,10 @@ export const useCreateController = <
: onSuccessRef.current
? onSuccessRef.current
: newRecord => {
- notify(
- successMessage ||
- 'ra.notification.created',
- {
- type: 'info',
- messageArgs: { smart_count: 1 },
- }
- );
+ notify('ra.notification.created', {
+ type: 'info',
+ messageArgs: { smart_count: 1 },
+ });
redirect(
redirectTo,
`/${resource}`,
@@ -164,7 +151,6 @@ export const useCreateController = <
onSuccessRef,
onFailureRef,
notify,
- successMessage,
redirect,
resource,
]
@@ -190,7 +176,6 @@ export const useCreateController = <
resource,
record: recordToUse,
redirect: getDefaultRedirectRoute(hasShow, hasEdit),
- version,
};
};
@@ -205,7 +190,6 @@ export interface CreateControllerProps<
unknown,
CreateParams
>;
- successMessage?: string;
transform?: TransformData;
}
@@ -234,11 +218,9 @@ export interface CreateControllerResult<
setOnSuccess: SetOnSuccess;
setOnFailure: SetOnFailure;
setTransform: SetTransformData;
- successMessage?: string;
record?: Partial;
redirect: RedirectionSideEffect;
resource: string;
- version: number;
}
const emptyRecord = {};
diff --git a/packages/ra-core/src/controller/edit/EditContext.tsx b/packages/ra-core/src/controller/edit/EditContext.tsx
index 54b289af068..023e879f1a0 100644
--- a/packages/ra-core/src/controller/edit/EditContext.tsx
+++ b/packages/ra-core/src/controller/edit/EditContext.tsx
@@ -35,7 +35,6 @@ export const EditContext = createContext({
resource: null,
save: null,
saving: null,
- version: null,
});
EditContext.displayName = 'EditContext';
diff --git a/packages/ra-core/src/controller/edit/useEditContext.tsx b/packages/ra-core/src/controller/edit/useEditContext.tsx
index e321e65c99d..cc3e3d137ee 100644
--- a/packages/ra-core/src/controller/edit/useEditContext.tsx
+++ b/packages/ra-core/src/controller/edit/useEditContext.tsx
@@ -64,8 +64,6 @@ const extractEditContextProps = ({
resource,
save,
saving,
- successMessage,
- version,
}: any) => ({
// Necessary for actions (EditActions) which expect a data prop containing the record
// @deprecated - to be removed in 4.0d
@@ -84,6 +82,4 @@ const extractEditContextProps = ({
resource,
save,
saving,
- successMessage,
- version,
});
diff --git a/packages/ra-core/src/controller/edit/useEditController.ts b/packages/ra-core/src/controller/edit/useEditController.ts
index 18a9819397b..24817f57b23 100644
--- a/packages/ra-core/src/controller/edit/useEditController.ts
+++ b/packages/ra-core/src/controller/edit/useEditController.ts
@@ -3,7 +3,6 @@ import { useParams } from 'react-router-dom';
import { UseQueryOptions, UseMutationOptions } from 'react-query';
import { useAuthenticated } from '../../auth';
-import useVersion from '../useVersion';
import {
Record,
MutationMode,
@@ -14,10 +13,14 @@ import {
import {
useNotify,
useRedirect,
- useRefresh,
RedirectionSideEffect,
} from '../../sideEffect';
-import { useGetOne, useUpdate, Refetch } from '../../dataProvider';
+import {
+ useGetOne,
+ useUpdate,
+ useRefresh,
+ UseGetOneHookValue,
+} from '../../dataProvider';
import { useTranslate } from '../../i18n';
import { useResourceContext, useGetResourceLabel } from '../../core';
import {
@@ -67,7 +70,6 @@ export const useEditController = (
const notify = useNotify();
const redirect = useRedirect();
const refresh = useRefresh();
- const version = useVersion();
const { id: routeId } = useParams<'id'>();
const id = propsId || decodeURIComponent(routeId);
const { onSuccess, onError, ...otherMutationOptions } = mutationOptions;
@@ -94,7 +96,8 @@ export const useEditController = (
redirect('list', `/${resource}`);
refresh();
},
-
+ refetchOnReconnect: false,
+ refetchOnWindowFocus: false,
retry: false,
...queryOptions,
}
@@ -213,7 +216,6 @@ export const useEditController = (
setOnSuccess,
setTransform,
transformRef,
- version,
};
};
@@ -259,10 +261,9 @@ export interface EditControllerResult {
setOnFailure: SetOnFailure;
setTransform: SetTransformData;
record?: RecordType;
- refetch: Refetch;
+ refetch: UseGetOneHookValue['refetch'];
redirect: RedirectionSideEffect;
resource: string;
- version: number;
}
const DefaultRedirect = 'list';
diff --git a/packages/ra-core/src/controller/index.ts b/packages/ra-core/src/controller/index.ts
index b5f5b970a33..1df3187aca2 100644
--- a/packages/ra-core/src/controller/index.ts
+++ b/packages/ra-core/src/controller/index.ts
@@ -1,5 +1,4 @@
import useRecordSelection from './useRecordSelection';
-import useVersion from './useVersion';
import useExpanded from './useExpanded';
import useFilterState from './useFilterState';
import useSortState, { SortProps } from './useSortState';
@@ -12,7 +11,6 @@ export type { PaginationHookResult, SortProps };
export {
useCheckMinimumRequiredProps,
useRecordSelection,
- useVersion,
useExpanded,
useFilterState,
usePaginationState,
diff --git a/packages/ra-core/src/controller/input/ReferenceArrayInputContext.ts b/packages/ra-core/src/controller/input/ReferenceArrayInputContext.ts
index 456473e9085..f5be4a456f7 100644
--- a/packages/ra-core/src/controller/input/ReferenceArrayInputContext.ts
+++ b/packages/ra-core/src/controller/input/ReferenceArrayInputContext.ts
@@ -16,8 +16,10 @@ import { PaginationPayload, Record, SortPayload } from '../../types';
*/
export const ReferenceArrayInputContext = createContext(undefined);
-export interface ReferenceArrayInputContextValue {
- choices: Record[];
+export interface ReferenceArrayInputContextValue<
+ RecordType extends Record = Record
+> {
+ choices: RecordType[];
error?: any;
warning?: any;
isLoading?: boolean;
diff --git a/packages/ra-core/src/controller/input/referenceDataStatus.ts b/packages/ra-core/src/controller/input/referenceDataStatus.ts
index 2677b323acb..4aca9163061 100644
--- a/packages/ra-core/src/controller/input/referenceDataStatus.ts
+++ b/packages/ra-core/src/controller/input/referenceDataStatus.ts
@@ -1,12 +1,12 @@
import { Record, Translate } from '../../types';
import { MatchingReferencesError } from './types';
-interface GetStatusForInputParams {
+interface GetStatusForInputParams {
input: {
value: any;
};
- matchingReferences: Record[] | MatchingReferencesError;
- referenceRecord: Record;
+ matchingReferences: RecordType[] | MatchingReferencesError;
+ referenceRecord: RecordType;
translate: Translate;
}
@@ -15,12 +15,12 @@ const isMatchingReferencesError = (
): matchingReferences is MatchingReferencesError =>
matchingReferences && matchingReferences.error !== undefined;
-export const getStatusForInput = ({
+export const getStatusForInput = ({
input,
matchingReferences,
referenceRecord,
translate = x => x,
-}: GetStatusForInputParams) => {
+}: GetStatusForInputParams) => {
const matchingReferencesError = isMatchingReferencesError(
matchingReferences
)
@@ -59,11 +59,11 @@ export const REFERENCES_STATUS_READY = 'REFERENCES_STATUS_READY';
export const REFERENCES_STATUS_INCOMPLETE = 'REFERENCES_STATUS_INCOMPLETE';
export const REFERENCES_STATUS_EMPTY = 'REFERENCES_STATUS_EMPTY';
-export const getSelectedReferencesStatus = (
+export const getSelectedReferencesStatus = (
input: {
value: any;
},
- referenceRecords: Record[]
+ referenceRecords: RecordType[]
) =>
!input.value || input.value.length === referenceRecords.length
? REFERENCES_STATUS_READY
@@ -71,21 +71,21 @@ export const getSelectedReferencesStatus = (
? REFERENCES_STATUS_INCOMPLETE
: REFERENCES_STATUS_EMPTY;
-interface GetStatusForArrayInputParams {
+interface GetStatusForArrayInputParams {
input: {
value: any;
};
- matchingReferences: Record[] | MatchingReferencesError;
- referenceRecords: Record[];
+ matchingReferences: RecordType[] | MatchingReferencesError;
+ referenceRecords: RecordType[];
translate: Translate;
}
-export const getStatusForArrayInput = ({
+export const getStatusForArrayInput = ({
input,
matchingReferences,
referenceRecords,
translate = x => x,
-}: GetStatusForArrayInputParams) => {
+}: GetStatusForArrayInputParams) => {
// selectedReferencesDataStatus can be "empty" (no data was found for references from input.value)
// or "incomplete" (Not all of the reference data was found)
// or "ready" (all references data was found or there is no references from input.value)
diff --git a/packages/ra-core/src/controller/input/useReferenceArrayInputController.ts b/packages/ra-core/src/controller/input/useReferenceArrayInputController.ts
index d64f3f30fe3..980ae4123c0 100644
--- a/packages/ra-core/src/controller/input/useReferenceArrayInputController.ts
+++ b/packages/ra-core/src/controller/input/useReferenceArrayInputController.ts
@@ -36,9 +36,11 @@ import { ReferenceArrayInputContextValue } from './ReferenceArrayInputContext';
*
* @return {Object} controllerProps Fetched data and callbacks for the ReferenceArrayInput components
*/
-export const useReferenceArrayInputController = (
- props: UseReferenceArrayInputParams
-): ReferenceArrayInputContextValue & Omit => {
+export const useReferenceArrayInputController = <
+ RecordType extends Record = Record
+>(
+ props: UseReferenceArrayInputParams
+): UseReferenceArrayInputControllerHookValue => {
const {
filter: defaultFilter,
filterToQuery = defaultFilterToQuery,
@@ -62,7 +64,9 @@ export const useReferenceArrayInputController = (
isLoading: isLoadingGetMany,
isFetching: isFetchingGetMany,
refetch: refetchGetMany,
- } = useGetManyAggregate(reference, { ids: input.value || EmptyArray });
+ } = useGetManyAggregate(reference, {
+ ids: input.value || EmptyArray,
+ });
/**
* Get the possible values to display as choices (with getList)
@@ -225,7 +229,7 @@ export const useReferenceArrayInputController = (
isLoading: isLoadingGetList,
isFetching: isFetchingGetList,
refetch: refetchGetMatching,
- } = useGetList(
+ } = useGetList(
reference,
{ pagination, sort, filter: finalFilter },
{ retry: false, enabled: isGetMatchingEnabled, ...options }
@@ -240,7 +244,7 @@ export const useReferenceArrayInputController = (
? finalReferenceRecords
: matchingReferences;
- const dataStatus = getDataStatus({
+ const dataStatus = getDataStatus({
input,
matchingReferences: finalMatchingReferences,
referenceRecords: finalReferenceRecords,
@@ -291,7 +295,10 @@ export const useReferenceArrayInputController = (
const EmptyArray = [];
// concatenate and deduplicate two lists of records
-const mergeReferences = (ref1: Record[], ref2: Record[]): Record[] => {
+const mergeReferences = (
+ ref1: RecordType[],
+ ref2: RecordType[]
+): RecordType[] => {
const res = [...ref1];
const ids = ref1.map(ref => ref.id);
ref2.forEach(ref => {
@@ -303,7 +310,9 @@ const mergeReferences = (ref1: Record[], ref2: Record[]): Record[] => {
return res;
};
-export interface UseReferenceArrayInputParams {
+export interface UseReferenceArrayInputParams<
+ RecordType extends Record = Record
+> {
basePath?: string;
filter?: any;
filterToQuery?: (filter: any) => any;
@@ -311,7 +320,7 @@ export interface UseReferenceArrayInputParams {
options?: any;
page?: number;
perPage?: number;
- record?: Record;
+ record?: RecordType;
reference: string;
resource?: string;
sort?: SortPayload;
@@ -319,4 +328,11 @@ export interface UseReferenceArrayInputParams {
enableGetChoices?: (filters: any) => boolean;
}
+export type UseReferenceArrayInputControllerHookValue<
+ RecordType extends Record = Record
+> = ReferenceArrayInputContextValue &
+ Omit, 'setSort' | 'refetch'> & {
+ refetch: () => void;
+ };
+
const defaultFilterToQuery = searchText => ({ q: searchText });
diff --git a/packages/ra-core/src/controller/input/useReferenceInputController.ts b/packages/ra-core/src/controller/input/useReferenceInputController.ts
index 48b08ff9c4b..7c700becce0 100644
--- a/packages/ra-core/src/controller/input/useReferenceInputController.ts
+++ b/packages/ra-core/src/controller/input/useReferenceInputController.ts
@@ -1,6 +1,6 @@
import { useCallback } from 'react';
-import { useGetList } from '../../dataProvider/useGetList';
+import { useGetList, UseGetManyHookValue } from '../../dataProvider';
import { getStatusForInput as getDataStatus } from './referenceDataStatus';
import useTranslate from '../../i18n/useTranslate';
import { PaginationPayload, Record, SortPayload } from '../../types';
@@ -11,7 +11,6 @@ import { useSortState } from '..';
import useFilterState from '../useFilterState';
import useSelectionState from '../useSelectionState';
import { useResourceContext } from '../../core';
-import { Refetch } from '../../dataProvider';
const defaultReferenceSource = (resource: string, source: string) =>
`${resource}@${source}`;
@@ -55,9 +54,9 @@ const defaultFilter = {};
* filterToQuery: searchText => ({ title: searchText })
* });
*/
-export const useReferenceInputController = (
+export const useReferenceInputController = (
props: UseReferenceInputControllerParams
-): ReferenceInputValue => {
+): ReferenceInputValue => {
const {
input,
page: initialPage = 1,
@@ -116,7 +115,7 @@ export const useReferenceInputController = (
isLoading: possibleValuesLoading,
error: possibleValuesError,
refetch: refetchGetList,
- } = useGetList(
+ } = useGetList(
reference,
{ pagination, sort, filter: filterValues },
{ enabled: enableGetChoices ? enableGetChoices(filterValues) : true }
@@ -129,12 +128,12 @@ export const useReferenceInputController = (
error: referenceError,
isLoading: referenceLoading,
isFetching: referenceFetching,
- } = useReference({
+ } = useReference({
id: input.value,
reference,
});
// add current value to possible sources
- let finalData: Record[], finalTotal: number;
+ let finalData: RecordType[], finalTotal: number;
if (
!referenceRecord ||
possibleValuesData.find(record => record.id === input.value)
@@ -182,7 +181,7 @@ export const useReferenceInputController = (
onSelect,
onToggleItem,
onUnselectItems,
- refetch,
+ refetch: refetchGetList,
resource,
},
referenceRecord: {
@@ -217,21 +216,21 @@ export const useReferenceInputController = (
const hideFilter = () => {};
const showFilter = () => {};
-export interface ReferenceInputValue {
- possibleValues: ListControllerResult;
+export interface ReferenceInputValue {
+ possibleValues: ListControllerResult;
referenceRecord: {
data?: Record;
isLoading: boolean;
isFetching: boolean;
error?: any;
- refetch: Refetch;
+ refetch: UseGetManyHookValue['refetch'];
};
dataStatus: {
error?: any;
loading: boolean;
warning?: string;
};
- choices: Record[];
+ choices: RecordType[];
error?: string;
isFetching: boolean;
isLoading: boolean;
@@ -242,7 +241,7 @@ export interface ReferenceInputValue {
setSort: (sort: SortPayload) => void;
sort: SortPayload;
warning?: string;
- refetch: Refetch;
+ refetch: () => void;
}
export interface UseReferenceInputControllerParams {
diff --git a/packages/ra-core/src/controller/list/useListController.spec.tsx b/packages/ra-core/src/controller/list/useListController.spec.tsx
index a2a11a9ad80..1a7beed1321 100644
--- a/packages/ra-core/src/controller/list/useListController.spec.tsx
+++ b/packages/ra-core/src/controller/list/useListController.spec.tsx
@@ -408,7 +408,6 @@ describe('useListController', () => {
showFilter: undefined,
total: undefined,
totalPages: undefined,
- version: undefined,
});
});
});
diff --git a/packages/ra-core/src/controller/list/useListController.ts b/packages/ra-core/src/controller/list/useListController.ts
index 1ba213297b2..68d943b8246 100644
--- a/packages/ra-core/src/controller/list/useListController.ts
+++ b/packages/ra-core/src/controller/list/useListController.ts
@@ -4,7 +4,7 @@ import { UseQueryOptions } from 'react-query';
import { useAuthenticated } from '../../auth';
import { useTranslate } from '../../i18n';
import { useNotify } from '../../sideEffect';
-import { useGetList, Refetch } from '../../dataProvider';
+import { useGetList, UseGetListHookValue } from '../../dataProvider';
import { SORT_ASC } from '../../reducer/admin/resource/list/queryReducer';
import { defaultExporter } from '../../export';
import { FilterPayload, SortPayload, Record, Exporter } from '../../types';
@@ -71,10 +71,6 @@ export const useListController = (
const [selectedIds, selectionModifiers] = useRecordSelection(resource);
- /**
- * We want the list of ids to be always available for optimistic rendering,
- * and therefore we need a custom action (CRUD_GET_LIST) that will be used.
- */
const { data, total, error, isLoading, isFetching, refetch } = useGetList<
RecordType
>(
@@ -197,7 +193,7 @@ export interface ListControllerResult {
onUnselectItems: () => void;
page: number;
perPage: number;
- refetch: Refetch;
+ refetch: UseGetListHookValue['refetch'];
resource: string;
selectedIds: RecordType['id'][];
setFilters: (
@@ -240,7 +236,6 @@ export const injectedProps = [
'showFilter',
'total',
'totalPages',
- 'version',
];
/**
diff --git a/packages/ra-core/src/controller/show/ShowContext.tsx b/packages/ra-core/src/controller/show/ShowContext.tsx
index 0670ed3f391..83f3e06f178 100644
--- a/packages/ra-core/src/controller/show/ShowContext.tsx
+++ b/packages/ra-core/src/controller/show/ShowContext.tsx
@@ -26,7 +26,6 @@ export const ShowContext = createContext({
isLoading: null,
refetch: null,
resource: null,
- version: null,
});
ShowContext.displayName = 'ShowContext';
diff --git a/packages/ra-core/src/controller/show/useShowContext.tsx b/packages/ra-core/src/controller/show/useShowContext.tsx
index f7fb92a76c8..b2fa3b0cf32 100644
--- a/packages/ra-core/src/controller/show/useShowContext.tsx
+++ b/packages/ra-core/src/controller/show/useShowContext.tsx
@@ -56,7 +56,6 @@ const extractShowContextProps = ({
isFetching,
isLoading,
resource,
- version,
}: any) => ({
basePath,
// Necessary for actions (EditActions) which expect a data prop containing the record
@@ -67,5 +66,4 @@ const extractShowContextProps = ({
isFetching,
isLoading,
resource,
- version,
});
diff --git a/packages/ra-core/src/controller/show/useShowController.ts b/packages/ra-core/src/controller/show/useShowController.ts
index 3d03d399b9f..ddba9b90cb4 100644
--- a/packages/ra-core/src/controller/show/useShowController.ts
+++ b/packages/ra-core/src/controller/show/useShowController.ts
@@ -2,11 +2,10 @@ import { useParams } from 'react-router-dom';
import { UseQueryOptions } from 'react-query';
import { useAuthenticated } from '../../auth';
-import useVersion from '../useVersion';
import { Record } from '../../types';
-import { useGetOne, Refetch } from '../../dataProvider';
+import { useGetOne, useRefresh, UseGetOneHookValue } from '../../dataProvider';
import { useTranslate } from '../../i18n';
-import { useNotify, useRedirect, useRefresh } from '../../sideEffect';
+import { useNotify, useRedirect } from '../../sideEffect';
import { useResourceContext, useGetResourceLabel } from '../../core';
/**
@@ -53,7 +52,6 @@ export const useShowController = (
const notify = useNotify();
const redirect = useRedirect();
const refresh = useRefresh();
- const version = useVersion();
const { id: routeId } = useParams<'id'>();
const id = propsId || decodeURIComponent(routeId);
@@ -97,7 +95,6 @@ export const useShowController = (
record,
refetch,
resource,
- version,
};
};
@@ -118,6 +115,5 @@ export interface ShowControllerResult {
isLoading: boolean;
resource: string;
record?: RecordType;
- refetch: Refetch;
- version: number;
+ refetch: UseGetOneHookValue['refetch'];
}
diff --git a/packages/ra-core/src/controller/useReference.ts b/packages/ra-core/src/controller/useReference.ts
index 7534a587a6a..82e830b583d 100644
--- a/packages/ra-core/src/controller/useReference.ts
+++ b/packages/ra-core/src/controller/useReference.ts
@@ -1,17 +1,17 @@
import { Record } from '../types';
-import { Refetch, useGetManyAggregate } from '../dataProvider';
+import { UseGetManyHookValue, useGetManyAggregate } from '../dataProvider';
interface UseReferenceProps {
id: string;
reference: string;
}
-export interface UseReferenceResult {
+export interface UseReferenceResult {
isLoading: boolean;
isFetching: boolean;
- referenceRecord?: Record;
+ referenceRecord?: RecordType;
error?: any;
- refetch: Refetch;
+ refetch: UseGetManyHookValue['refetch'];
}
/**
@@ -41,17 +41,13 @@ export interface UseReferenceResult {
*
* @returns {UseReferenceResult} The reference record
*/
-export const useReference = ({
+export const useReference = ({
reference,
id,
-}: UseReferenceProps): UseReferenceResult => {
- const {
- data,
- error,
- isLoading,
- isFetching,
- refetch,
- } = useGetManyAggregate(reference, { ids: [id] });
+}: UseReferenceProps): UseReferenceResult => {
+ const { data, error, isLoading, isFetching, refetch } = useGetManyAggregate<
+ RecordType
+ >(reference, { ids: [id] });
return {
referenceRecord: error ? undefined : data ? data[0] : undefined,
refetch,
diff --git a/packages/ra-core/src/controller/useVersion.ts b/packages/ra-core/src/controller/useVersion.ts
deleted file mode 100644
index 25ba43e85a2..00000000000
--- a/packages/ra-core/src/controller/useVersion.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { useSelector } from 'react-redux';
-import { ReduxState } from '../types';
-
-/**
- * Get the UI version from the store
- *
- * The UI version is an integer incremented by the refresh button;
- * it serves to force running fetch hooks again.
- */
-const useVersion = () =>
- useSelector((reduxState: ReduxState) => reduxState.admin.ui.viewVersion);
-
-export default useVersion;
diff --git a/packages/ra-core/src/dataProvider/Mutation.spec.tsx b/packages/ra-core/src/dataProvider/Mutation.spec.tsx
deleted file mode 100644
index 2e78b0f2b1c..00000000000
--- a/packages/ra-core/src/dataProvider/Mutation.spec.tsx
+++ /dev/null
@@ -1,145 +0,0 @@
-import * as React from 'react';
-import { fireEvent, waitFor, act, render } from '@testing-library/react';
-import expect from 'expect';
-
-import Mutation from './Mutation';
-import { showNotification } from '../actions';
-import DataProviderContext from './DataProviderContext';
-import { renderWithRedux, TestContext } from 'ra-test';
-import { useNotify } from '../sideEffect';
-
-describe('Mutation', () => {
- it('should render its child function', () => {
- const { getByTestId } = renderWithRedux(
-
- {() => Hello
}
-
- );
- expect(getByTestId('test').textContent).toBe('Hello');
- });
-
- it('should pass useEditController return value to child', () => {
- let callback = null;
- let state = null;
- renderWithRedux(
-
- {(mutate, controllerState) => {
- callback = mutate;
- state = controllerState;
- return Hello
;
- }}
-
- );
- expect(callback).toBeInstanceOf(Function);
- expect(state).toEqual({
- data: null,
- error: null,
- total: null,
- loaded: false,
- loading: false,
- });
- });
-
- it('supports onSuccess side effects using hooks', async () => {
- let dispatchSpy;
- const dataProvider = {
- mytype: jest.fn(() => Promise.resolve({ data: { foo: 'bar' } })),
- };
-
- const Foo = () => {
- const notify = useNotify();
- return (
- {
- notify('Youhou!');
- },
- }}
- >
- {(mutate, { data }) => (
-
- {data ? data.foo : 'no data'}
-
- )}
-
- );
- };
- let getByTestId;
- act(() => {
- const res = render(
-
-
- {({ store }) => {
- dispatchSpy = jest.spyOn(store, 'dispatch');
- return ;
- }}
-
-
- );
- getByTestId = res.getByTestId;
- });
-
- const testElement = getByTestId('test');
- fireEvent.click(testElement);
- await waitFor(() => {
- expect(dispatchSpy).toHaveBeenCalledWith(
- showNotification('Youhou!', 'info')
- );
- });
- });
-
- it('supports onFailure side effects using hooks', async () => {
- jest.spyOn(console, 'error').mockImplementationOnce(() => {});
- let dispatchSpy;
- const dataProvider = {
- mytype: jest.fn(() =>
- Promise.reject({ message: 'provider error' })
- ),
- };
-
- const Foo = () => {
- const notify = useNotify();
- return (
- {
- notify('Damn!', { type: 'warning' });
- },
- }}
- >
- {(mutate, { error }) => (
-
- {error ? error.message : 'no data'}
-
- )}
-
- );
- };
- let getByTestId;
- act(() => {
- const res = render(
-
-
- {({ store }) => {
- dispatchSpy = jest.spyOn(store, 'dispatch');
- return ;
- }}
-
-
- );
- getByTestId = res.getByTestId;
- });
-
- const testElement = getByTestId('test');
- fireEvent.click(testElement);
- await waitFor(() => {
- expect(dispatchSpy).toHaveBeenCalledWith(
- showNotification('Damn!', 'warning')
- );
- });
- });
-});
diff --git a/packages/ra-core/src/dataProvider/Mutation.tsx b/packages/ra-core/src/dataProvider/Mutation.tsx
deleted file mode 100644
index 2a0ea1ac508..00000000000
--- a/packages/ra-core/src/dataProvider/Mutation.tsx
+++ /dev/null
@@ -1,65 +0,0 @@
-import useMutation from './useMutation';
-
-interface ChildrenFuncParams {
- data?: any;
- total?: number;
- error?: any;
- loading: boolean;
- loaded: boolean;
-}
-
-export interface MutationProps {
- children: (
- mutate: (
- event?: any,
- callTimePayload?: any,
- callTimeOptions?: any
- ) => void | Promise,
- params: ChildrenFuncParams
- ) => JSX.Element;
- type: string;
- resource?: string;
- payload?: any;
- options?: any;
-}
-
-/**
- * Get a callback to call the data provider and pass the result to a child function
- *
- * @param {Function} children Must be a function which will be called with the mutate callback
- * @param {string} type The method called on the data provider, e.g. 'update', 'delete'. Can also be a custom method if the dataProvider supports is.
- * @param {string} resource A resource name, e.g. 'posts', 'comments'
- * @param {Object} payload The payload object, e.g; { id: 12 }
- * @param {Object} options
- * @param {string} options.action Redux action type
- * @param {boolean} options.mutationMode Either 'optimistic', 'pessimistic' or 'undoable'
- * @param {boolean} options.returnPromise Set to true to return the result promise of the mutation
- * @param {Function} options.onSuccess Side effect function to be executed upon success or failure, e.g. { onSuccess: response => refresh() }
- * @param {Function} options.onFailure Side effect function to be executed upon failure, e.g. { onFailure: error => notify(error.message) }
- *
- * @example
- *
- * const ApproveButton = ({ record }) => (
- *
- * {approve => (
- *
- * )}
- *
- * );
- */
-const Mutation = ({
- children,
- type,
- resource,
- payload,
- // Provides an undefined onSuccess just so the key `onSuccess` is defined
- // This is used to detect options in useDataProvider
- options = { onSuccess: undefined },
-}: MutationProps) =>
- children(...useMutation({ type, resource, payload }, options));
-
-export default Mutation;
diff --git a/packages/ra-core/src/dataProvider/Query.spec.tsx b/packages/ra-core/src/dataProvider/Query.spec.tsx
deleted file mode 100644
index 86b2ebb0606..00000000000
--- a/packages/ra-core/src/dataProvider/Query.spec.tsx
+++ /dev/null
@@ -1,434 +0,0 @@
-import * as React from 'react';
-import { FC } from 'react';
-import {
- act,
- fireEvent,
- render,
- screen,
- waitFor,
-} from '@testing-library/react';
-import expect from 'expect';
-import { Provider } from 'react-redux';
-
-import Query from './Query';
-import { createAdminStore, CoreAdminContext, Resource } from '../core';
-import { testDataProvider } from '../dataProvider';
-import { showNotification } from '../actions';
-import { useNotify, useRefresh } from '../sideEffect';
-
-describe('Query', () => {
- it('should render its child', () => {
- render(
-
-
- {() => Hello
}
-
-
- );
- expect(screen.getByTestId('test').textContent).toBe('Hello');
- });
-
- it('should dispatch a fetch action when mounting', () => {
- const store = createAdminStore({});
- let dispatch = jest.spyOn(store, 'dispatch');
- const myPayload = {};
-
- render(
-
-
-
- {() => Hello
}
-
-
-
- );
-
- const action = dispatch.mock.calls[0][0];
- expect(action.type).toEqual('CUSTOM_FETCH');
- expect(action.payload).toEqual(myPayload);
- expect(action.meta.resource).toEqual('myresource');
- });
-
- it('should set the loading state to loading when mounting', () => {
- const myPayload = {};
- render(
-
-
- {({ loading }) => (
-
- Hello
-
- )}
-
-
- );
- expect(screen.getByText('Hello').className).toEqual('loading');
- });
-
- it('should update the data state after a success response', async () => {
- const dataProvider = {
- mytype: jest.fn(() => Promise.resolve({ data: { foo: 'bar' } })),
- };
- const Foo = () => (
-
- {({ loading, data }) => (
-
- {data ? data.foo : 'no data'}
-
- )}
-
- );
- render(
-
-
-
- );
- const testElement = screen.getByTestId('test');
- expect(testElement.textContent).toBe('no data');
- expect(testElement.className).toEqual('loading');
-
- await waitFor(() => {
- expect(testElement.textContent).toEqual('bar');
- expect(testElement.className).toEqual('idle');
- });
- });
-
- it('should return the total prop if available', async () => {
- const dataProvider = {
- mytype: jest.fn(() =>
- Promise.resolve({ data: { foo: 'bar' }, total: 42 })
- ),
- };
-
- const Foo = () => (
-
- {({ loading, data, total }) => (
-
- {loading ? 'no data' : total}
-
- )}
-
- );
- render(
-
-
-
- );
- const testElement = screen.getByTestId('test');
- expect(testElement.className).toEqual('loading');
- expect(testElement.textContent).toBe('no data');
-
- await waitFor(() => {
- expect(testElement.className).toEqual('idle');
- expect(testElement.textContent).toEqual('42');
- });
- });
-
- it('should update the error state after an error response', async () => {
- jest.spyOn(console, 'error').mockImplementationOnce(() => {});
- const dataProvider = {
- getList: jest.fn(() =>
- Promise.reject({ message: 'provider error' })
- ),
- };
- const Foo = () => (
-
- {({ loading, error }) => (
-
- {error ? error.message : 'no data'}
-
- )}
-
- );
- render(
-
-
-
- );
-
- const testElement = screen.getByTestId('test');
- expect(testElement.textContent).toBe('no data');
- expect(testElement.className).toEqual('loading');
-
- await waitFor(() => {
- expect(testElement.textContent).toEqual('provider error');
- expect(testElement.className).toEqual('idle');
- });
- });
-
- it('should dispatch a new fetch action when updating', () => {
- const store = createAdminStore({});
- let dispatch = jest.spyOn(store, 'dispatch');
- const myPayload = {};
- const { rerender } = render(
-
-
-
- {() => Hello
}
-
-
-
- );
- expect(dispatch.mock.calls.length).toEqual(3);
- const mySecondPayload = { foo: 1 };
- act(() => {
- rerender(
-
-
- Promise.resolve({ data: [], total: 0 })
- }
- >
-
- {() => Hello
}
-
-
-
- );
- });
- expect(dispatch.mock.calls.length).toEqual(6);
- const action = dispatch.mock.calls[3][0];
- expect(action.type).toEqual('CUSTOM_FETCH');
- expect(action.payload).toEqual(mySecondPayload);
- expect(action.meta.resource).toEqual('myresource');
- });
-
- it('should not dispatch a new fetch action when updating with the same query props', () => {
- const store = createAdminStore({});
- let dispatch = jest.spyOn(store, 'dispatch');
- const dataProvider = {
- getList: () => Promise.resolve({ data: [], total: 0 }),
- };
- const Wrapper: FC = ({ children }) => (
-
-
- {children}
-
-
- );
- const { rerender } = render(
-
- {() => Hello
}
- ,
- { wrapper: Wrapper }
- );
- expect(dispatch.mock.calls.length).toEqual(3);
- rerender(
-
- {() => Hello
}
-
- );
- expect(dispatch.mock.calls.length).toEqual(3);
- });
-
- it('supports onSuccess function for side effects', async () => {
- const store = createAdminStore({});
- let dispatch = jest.spyOn(store, 'dispatch');
- const dataProvider = {
- getList: jest.fn(() =>
- Promise.resolve({ data: [{ id: 1, foo: 'bar' }], total: 42 })
- ),
- };
-
- const Foo = () => {
- const notify = useNotify();
- return (
- {
- notify('Youhou!');
- },
- }}
- >
- {({ loading, data, total }) => (
-
- {loading ? 'no data' : total}
-
- )}
-
- );
- };
-
- act(() => {
- render(
-
-
-
-
-
- );
- });
-
- await waitFor(() => {
- expect(dispatch).toHaveBeenCalledWith(
- showNotification('Youhou!', 'info')
- );
- });
-
- dispatch.mockRestore();
- });
-
- it('supports onFailure function for side effects', async () => {
- jest.spyOn(console, 'error').mockImplementationOnce(() => {});
- const store = createAdminStore({});
- let dispatch = jest.spyOn(store, 'dispatch');
- const dataProvider = {
- getList: jest.fn(() =>
- Promise.reject({ message: 'provider error' })
- ),
- };
-
- const Foo = () => {
- const notify = useNotify();
- return (
- {
- notify('Damn!', { type: 'warning' });
- },
- }}
- >
- {({ loading, data, total }) => (
-
- {loading ? 'no data' : total}
-
- )}
-
- );
- };
- act(() => {
- render(
-
-
-
-
-
- );
- });
-
- await waitFor(() => {
- expect(dispatch).toHaveBeenCalledWith(
- showNotification('Damn!', 'warning')
- );
- });
-
- dispatch.mockRestore();
- });
-
- it('should fetch again when refreshing', async () => {
- const store = createAdminStore({});
- let dispatch = jest.spyOn(store, 'dispatch');
-
- const dataProvider = {
- mytype: jest.fn(() => Promise.resolve({ data: { foo: 'bar' } })),
- };
-
- const Button = () => {
- const refresh = useRefresh();
- return (
- refresh()}>
- Click me
-
- );
- };
-
- render(
-
-
-
- {() => }
-
-
-
- );
-
- await waitFor(() => {
- expect(dispatch).toHaveBeenCalledWith({
- type: 'CUSTOM_FETCH',
- payload: undefined,
- meta: { resource: 'foo' },
- });
- });
- dispatch.mockClear(); // clear initial fetch
-
- const testElement = screen.getByTestId('test');
- fireEvent.click(testElement);
- await waitFor(() => {
- expect(dispatch).toHaveBeenCalledWith({
- type: 'CUSTOM_FETCH',
- payload: undefined,
- meta: { resource: 'foo' },
- });
- });
- });
-
- it('should allow custom dataProvider methods without resource', () => {
- const store = createAdminStore({});
- let dispatch = jest.spyOn(store, 'dispatch');
- const dataProvider = {
- mytype: jest.fn(() => Promise.resolve({ data: { foo: 'bar' } })),
- };
-
- const myPayload = {};
- render(
-
-
-
- {() =>
}
-
-
-
- );
- const action = dispatch.mock.calls[0][0];
- expect(action.type).toEqual('CUSTOM_FETCH');
- expect(action.meta.resource).toBeUndefined();
- expect(dataProvider.mytype).toHaveBeenCalledWith(myPayload);
- });
-});
diff --git a/packages/ra-core/src/dataProvider/Query.tsx b/packages/ra-core/src/dataProvider/Query.tsx
deleted file mode 100644
index e23abdd2f0c..00000000000
--- a/packages/ra-core/src/dataProvider/Query.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import { FunctionComponent } from 'react';
-import { useQuery } from './useQuery';
-
-interface ChildrenFuncParams {
- data?: any;
- total?: number;
- loading: boolean;
- loaded: boolean;
- error?: any;
-}
-
-export interface QueryProps {
- children: (params: ChildrenFuncParams) => JSX.Element;
- type: string;
- resource?: string;
- payload?: any;
- options?: any;
-}
-
-/**
- * Fetch the data provider and pass the result to a child function
- *
- * @param {Function} children Must be a function which will be called with an object containing the following keys: data, loading and error
- * @param {string} type The method called on the data provider, e.g. 'getList', 'getOne'. Can also be a custom method if the dataProvider supports is.
- * @param {string} resource A resource name, e.g. 'posts', 'comments'
- * @param {Object} payload The payload object, e.g; { post_id: 12 }
- * @param {Object} options
- * @param {string} options.action Redux action type
- * @param {Function} options.onSuccess Side effect function to be executed upon success or failure, e.g. { onSuccess: response => refresh() }
- * @param {Function} options.onFailure Side effect function to be executed upon failure, e.g. { onFailure: error => notify(error.message) }
- *
- * This component also supports legacy side effects (e.g. { onSuccess: { refresh: true } })
- *
- * @example
- *
- * const UserProfile = ({ record }) => (
- *
- * {({ data, loading, error }) => {
- * if (loading) { return ; }
- * if (error) { return ERROR
; }
- * return User {data.username}
;
- * }}
- *
- * );
- *
- * @example
- *
- * const payload = {
- * pagination: { page: 1, perPage: 10 },
- * sort: { field: 'username', order: 'ASC' },
- * };
- * const UserList = () => (
- *
- * {({ data, total, loading, error }) => {
- * if (loading) { return ; }
- * if (error) { return ERROR
; }
- * return (
- *
- *
Total users: {total}
- *
- * {data.map(user => {user.username} )}
- *
- *
- * );
- * }}
- *
- * );
- */
-const Query: FunctionComponent = ({
- children,
- type,
- resource,
- payload,
- // Provides an undefined onSuccess just so the key `onSuccess` is defined
- // This is used to detect options in useDataProvider
- options = { onSuccess: undefined },
-}) => children(useQuery({ type, resource, payload }, options));
-
-export default Query;
diff --git a/packages/ra-core/src/dataProvider/cacheDataProviderProxy.ts b/packages/ra-core/src/dataProvider/cacheDataProviderProxy.ts
deleted file mode 100644
index 102aae38a5d..00000000000
--- a/packages/ra-core/src/dataProvider/cacheDataProviderProxy.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import { DataProvider } from '../types';
-
-const fiveMinutes = 5 * 60 * 1000;
-
-/**
- * Wrap a dataProvider in a Proxy that modifies responses to add caching.
- *
- * This proxy adds a validUntil field to the response of read queries
- * (getList, getMany, getOne) so that the useDataProvider enables caching
- * for these calls.
- *
- * @param {DataProvider} dataProvider A data provider object
- * @param {number} duration Cache duration in milliseconds. Defaults to 5 minutes.
- *
- * @example
- *
- * import { cacheDataProviderProxy } from 'ra-core';
- *
- * const cacheEnabledDataProvider = cacheDataProviderProxy(dataProvider);
- */
-export default (
- dataProvider: DataProvider,
- duration: number = fiveMinutes
-): DataProvider =>
- new Proxy(dataProvider, {
- get: (target, name: string) => {
- if (typeof name === 'symbol') {
- return;
- }
- return (resource, params) => {
- if (
- name === 'getList' ||
- name === 'getMany' ||
- name === 'getOne'
- ) {
- // @ts-ignore
- return dataProvider[name](resource, params).then(
- response => {
- const validUntil = new Date();
- validUntil.setTime(validUntil.getTime() + duration);
- response.validUntil = validUntil;
- return response;
- }
- );
- }
- return dataProvider[name](resource, params);
- };
- },
- });
diff --git a/packages/ra-core/src/dataProvider/getDataProviderCallArguments.ts b/packages/ra-core/src/dataProvider/getDataProviderCallArguments.ts
deleted file mode 100644
index 8770421c93e..00000000000
--- a/packages/ra-core/src/dataProvider/getDataProviderCallArguments.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import { UseDataProviderOptions } from '../types';
-
-// List of properties we expect in the options
-const OptionsProperties = [
- 'action',
- 'fetch',
- 'meta',
- 'onFailure',
- 'onSuccess',
- 'mutationMode',
- 'enabled',
-];
-
-const isDataProviderOptions = (value: any) => {
- if (typeof value === 'undefined') return [];
- let options = value as UseDataProviderOptions;
-
- return Object.keys(options).some(key => OptionsProperties.includes(key));
-};
-
-// As all dataProvider methods do not have the same signature, we must differentiate
-// standard methods which have the (resource, params, options) signature
-// from the custom ones
-export const getDataProviderCallArguments = (
- args: any[]
-): {
- resource: string;
- payload: any;
- options: UseDataProviderOptions;
- allArguments: any[];
-} => {
- const lastArg = args[args.length - 1];
- let allArguments = [...args];
-
- let resource;
- let payload;
- let options;
-
- if (isDataProviderOptions(lastArg)) {
- options = lastArg as UseDataProviderOptions;
- allArguments = allArguments.slice(0, args.length - 1);
- }
-
- if (typeof allArguments[0] === 'string') {
- resource = allArguments[0];
- payload = allArguments[1];
- }
-
- return {
- resource,
- payload,
- allArguments,
- options,
- };
-};
diff --git a/packages/ra-core/src/dataProvider/index.ts b/packages/ra-core/src/dataProvider/index.ts
index 71e9c68e542..a84daf71c0d 100644
--- a/packages/ra-core/src/dataProvider/index.ts
+++ b/packages/ra-core/src/dataProvider/index.ts
@@ -2,24 +2,18 @@ import convertLegacyDataProvider from './convertLegacyDataProvider';
import DataProviderContext from './DataProviderContext';
import HttpError from './HttpError';
import * as fetchUtils from './fetch';
-import Mutation, { MutationProps } from './Mutation';
-import Query, { QueryProps } from './Query';
-import cacheDataProviderProxy from './cacheDataProviderProxy';
import undoableEventEmitter from './undoableEventEmitter';
-import useDataProvider from './useDataProvider';
-import useMutation, { UseMutationValue } from './useMutation';
import withDataProvider from './withDataProvider';
-import useRefreshWhenVisible from './useRefreshWhenVisible';
-import useIsAutomaticRefreshEnabled from './useIsAutomaticRefreshEnabled';
export * from './testDataProvider';
+export * from './useDataProvider';
+export * from './useLoading';
+export * from './useRefresh';
export * from './useGetOne';
export * from './useGetList';
export * from './useGetMany';
export * from './useGetManyAggregate';
export * from './useGetManyReference';
-export * from './useQueryWithStore';
-export * from './useQuery';
export * from './useCreate';
export * from './useUpdate';
export * from './useUpdateMany';
@@ -27,20 +21,12 @@ export * from './useDelete';
export * from './useDeleteMany';
export type { Options } from './fetch';
-export type { QueryProps, UseMutationValue, MutationProps };
export {
- cacheDataProviderProxy,
convertLegacyDataProvider,
DataProviderContext,
fetchUtils,
HttpError,
- Mutation,
- Query,
undoableEventEmitter,
- useDataProvider,
- useMutation,
- useRefreshWhenVisible,
withDataProvider,
- useIsAutomaticRefreshEnabled,
};
diff --git a/packages/ra-core/src/dataProvider/performQuery/QueryFunctionParams.ts b/packages/ra-core/src/dataProvider/performQuery/QueryFunctionParams.ts
deleted file mode 100644
index 7dafc9794ef..00000000000
--- a/packages/ra-core/src/dataProvider/performQuery/QueryFunctionParams.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { Dispatch } from 'redux';
-import { DataProvider, OnSuccess, OnFailure } from '../../types';
-
-export interface QueryFunctionParams {
- /** The fetch type, e.g. `UPDATE_MANY` */
- type: string;
- payload: any;
- resource: string;
- /** The root action name, e.g. `CRUD_GET_MANY` */
- action: string;
- rest?: {
- fetch?: string;
- meta?: object;
- };
- onSuccess?: OnSuccess;
- onFailure?: OnFailure;
- dataProvider: DataProvider;
- dispatch: Dispatch;
- logoutIfAccessDenied: (error?: any) => Promise;
- allArguments: any[];
-}
diff --git a/packages/ra-core/src/dataProvider/performQuery/answerWithCache.ts b/packages/ra-core/src/dataProvider/performQuery/answerWithCache.ts
deleted file mode 100644
index d721c1d5259..00000000000
--- a/packages/ra-core/src/dataProvider/performQuery/answerWithCache.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import { getResultFromCache } from '../replyWithCache';
-import getFetchType from '../getFetchType';
-import { FETCH_END } from '../../actions/fetchActions';
-
-export const answerWithCache = ({
- type,
- payload,
- resource,
- action,
- rest,
- onSuccess,
- resourceState,
- dispatch,
-}) => {
- dispatch({
- type: action,
- payload,
- meta: { resource, ...rest },
- });
- const response = getResultFromCache(type, payload, resourceState);
- dispatch({
- type: `${action}_SUCCESS`,
- payload: response,
- requestPayload: payload,
- meta: {
- ...rest,
- resource,
- fetchResponse: getFetchType(type),
- fetchStatus: FETCH_END,
- fromCache: true,
- },
- });
- onSuccess && onSuccess(response);
- return Promise.resolve(response);
-};
diff --git a/packages/ra-core/src/dataProvider/performQuery/doQuery.ts b/packages/ra-core/src/dataProvider/performQuery/doQuery.ts
deleted file mode 100644
index 79a8544549e..00000000000
--- a/packages/ra-core/src/dataProvider/performQuery/doQuery.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-import { performOptimisticQuery } from './performOptimisticQuery';
-import { performUndoableQuery } from './performUndoableQuery';
-import { performPessimisticQuery } from './performPessimisticQuery';
-import { QueryFunctionParams } from './QueryFunctionParams';
-import { MutationMode } from '../../types';
-
-/**
- * Execute a dataProvider call
- *
- * Delegates execution to cache, optimistic, undoable, or pessimistic queries
- *
- * @see useDataProvider
- */
-export const doQuery = ({
- type,
- payload,
- resource,
- action,
- rest,
- onSuccess,
- onFailure,
- dataProvider,
- dispatch,
- logoutIfAccessDenied,
- allArguments,
- store,
- mutationMode,
-}: DoQueryParameters) => {
- if (mutationMode === 'optimistic') {
- return performOptimisticQuery({
- type,
- payload,
- resource,
- action,
- rest,
- onSuccess,
- onFailure,
- dataProvider,
- dispatch,
- logoutIfAccessDenied,
- allArguments,
- });
- } else if (mutationMode === 'undoable') {
- return performUndoableQuery({
- type,
- payload,
- resource,
- action,
- rest,
- onSuccess,
- onFailure,
- dataProvider,
- dispatch,
- logoutIfAccessDenied,
- allArguments,
- });
- } else {
- return performPessimisticQuery({
- type,
- payload,
- resource,
- action,
- rest,
- onSuccess,
- onFailure,
- dataProvider,
- dispatch,
- logoutIfAccessDenied,
- allArguments,
- });
- }
-};
-
-interface DoQueryParameters extends QueryFunctionParams {
- store: any; // unfortunately react-redux doesn't expose Store and AnyAction types, so we can't do better
- mutationMode: MutationMode;
-}
diff --git a/packages/ra-core/src/dataProvider/performQuery/index.ts b/packages/ra-core/src/dataProvider/performQuery/index.ts
deleted file mode 100644
index 9e31831e60d..00000000000
--- a/packages/ra-core/src/dataProvider/performQuery/index.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { doQuery } from './doQuery';
-import {
- stackCall,
- stackOptimisticCall,
- getRemainingStackedCalls,
-} from './stackedCalls';
-
-export { doQuery, stackCall, stackOptimisticCall, getRemainingStackedCalls };
diff --git a/packages/ra-core/src/dataProvider/performQuery/performOptimisticQuery.ts b/packages/ra-core/src/dataProvider/performQuery/performOptimisticQuery.ts
deleted file mode 100644
index e016658de3b..00000000000
--- a/packages/ra-core/src/dataProvider/performQuery/performOptimisticQuery.ts
+++ /dev/null
@@ -1,119 +0,0 @@
-import validateResponseFormat from '../validateResponseFormat';
-import getFetchType from '../getFetchType';
-import {
- startOptimisticMode,
- stopOptimisticMode,
-} from '../../actions/undoActions';
-import {
- FETCH_END,
- FETCH_ERROR,
- FETCH_START,
-} from '../../actions/fetchActions';
-import { replayStackedCalls } from './stackedCalls';
-import { QueryFunctionParams } from './QueryFunctionParams';
-
-/**
- * In optimistic mode, the useDataProvider hook dispatches an optimistic action
- * and executes the success side effects right away. Then it immediately calls
- * the dataProvider.
- *
- * We call that "optimistic" because the hook returns a resolved Promise
- * immediately (although it has an empty value). That only works if the
- * caller reads the result from the Redux store, not from the Promise.
- */
-export const performOptimisticQuery = ({
- type,
- payload,
- resource,
- action,
- rest,
- onSuccess,
- onFailure,
- dataProvider,
- dispatch,
- logoutIfAccessDenied,
- allArguments,
-}: QueryFunctionParams): Promise<{}> => {
- dispatch(startOptimisticMode());
- dispatch({
- type: action,
- payload,
- meta: { resource, ...rest },
- });
- dispatch({
- type: `${action}_OPTIMISTIC`,
- payload,
- meta: {
- resource,
- fetch: getFetchType(type),
- optimistic: true,
- },
- });
- onSuccess && onSuccess({});
- setTimeout(() => {
- dispatch(stopOptimisticMode());
- dispatch({
- type: `${action}_LOADING`,
- payload,
- meta: { resource, ...rest },
- });
- dispatch({ type: FETCH_START });
- try {
- dataProvider[type]
- .apply(
- dataProvider,
- typeof resource !== 'undefined'
- ? [resource, payload]
- : allArguments
- )
- .then(response => {
- if (process.env.NODE_ENV !== 'production') {
- validateResponseFormat(response, type);
- }
- dispatch({
- type: `${action}_SUCCESS`,
- payload: response,
- requestPayload: payload,
- meta: {
- ...rest,
- resource,
- fetchResponse: getFetchType(type),
- fetchStatus: FETCH_END,
- },
- });
- dispatch({ type: FETCH_END });
- replayStackedCalls();
- })
- .catch(error => {
- if (process.env.NODE_ENV !== 'production') {
- console.error(error);
- }
- return logoutIfAccessDenied(error).then(loggedOut => {
- if (loggedOut) return;
- dispatch({
- type: `${action}_FAILURE`,
- error: error.message ? error.message : error,
- payload: error.body ? error.body : null,
- requestPayload: payload,
- meta: {
- ...rest,
- resource,
- fetchResponse: getFetchType(type),
- fetchStatus: FETCH_ERROR,
- },
- });
- dispatch({ type: FETCH_ERROR, error });
- onFailure && onFailure(error);
- });
- });
- } catch (e) {
- if (process.env.NODE_ENV !== 'production') {
- console.error(e);
- }
- throw new Error(
- 'The dataProvider threw an error. It should return a rejected Promise instead.'
- );
- }
- });
- return Promise.resolve({});
-};
diff --git a/packages/ra-core/src/dataProvider/performQuery/performPessimisticQuery.ts b/packages/ra-core/src/dataProvider/performQuery/performPessimisticQuery.ts
deleted file mode 100644
index 9daeec1b245..00000000000
--- a/packages/ra-core/src/dataProvider/performQuery/performPessimisticQuery.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-import validateResponseFormat from '../validateResponseFormat';
-import getFetchType from '../getFetchType';
-import {
- FETCH_END,
- FETCH_ERROR,
- FETCH_START,
-} from '../../actions/fetchActions';
-import { QueryFunctionParams } from './QueryFunctionParams';
-
-/**
- * In pessimistic mode, the useDataProvider hook calls the dataProvider. When a
- * successful response arrives, the hook dispatches a SUCCESS action, executes
- * success side effects and returns the response. If the response is an error,
- * the hook dispatches a FAILURE action, executes failure side effects, and
- * throws an error.
- */
-export const performPessimisticQuery = ({
- type,
- payload,
- resource,
- action,
- rest,
- onSuccess,
- onFailure,
- dataProvider,
- dispatch,
- logoutIfAccessDenied,
- allArguments,
-}: QueryFunctionParams): Promise => {
- dispatch({
- type: action,
- payload,
- meta: { resource, ...rest },
- });
- dispatch({
- type: `${action}_LOADING`,
- payload,
- meta: { resource, ...rest },
- });
- dispatch({ type: FETCH_START });
-
- try {
- return dataProvider[type]
- .apply(
- dataProvider,
- typeof resource !== 'undefined'
- ? [resource, payload]
- : allArguments
- )
- .then(response => {
- if (process.env.NODE_ENV !== 'production') {
- validateResponseFormat(response, type);
- }
- dispatch({
- type: `${action}_SUCCESS`,
- payload: response,
- requestPayload: payload,
- meta: {
- ...rest,
- resource,
- fetchResponse: getFetchType(type),
- fetchStatus: FETCH_END,
- },
- });
- dispatch({ type: FETCH_END });
- onSuccess && onSuccess(response);
- return response;
- })
- .catch(error => {
- if (process.env.NODE_ENV !== 'production') {
- console.error(error);
- }
- return logoutIfAccessDenied(error).then(loggedOut => {
- if (loggedOut) return;
- dispatch({
- type: `${action}_FAILURE`,
- error: error.message ? error.message : error,
- payload: error.body ? error.body : null,
- requestPayload: payload,
- meta: {
- ...rest,
- resource,
- fetchResponse: getFetchType(type),
- fetchStatus: FETCH_ERROR,
- },
- });
- dispatch({ type: FETCH_ERROR, error });
- onFailure && onFailure(error);
- throw error;
- });
- });
- } catch (e) {
- if (process.env.NODE_ENV !== 'production') {
- console.error(e);
- }
- throw new Error(
- 'The dataProvider threw an error. It should return a rejected Promise instead.'
- );
- }
-};
diff --git a/packages/ra-core/src/dataProvider/performQuery/performUndoableQuery.ts b/packages/ra-core/src/dataProvider/performQuery/performUndoableQuery.ts
deleted file mode 100644
index 8716f80d64f..00000000000
--- a/packages/ra-core/src/dataProvider/performQuery/performUndoableQuery.ts
+++ /dev/null
@@ -1,166 +0,0 @@
-import validateResponseFormat from '../validateResponseFormat';
-import getFetchType from '../getFetchType';
-import undoableEventEmitter from '../undoableEventEmitter';
-import {
- startOptimisticMode,
- stopOptimisticMode,
-} from '../../actions/undoActions';
-import { showNotification } from '../../actions/notificationActions';
-import { refreshView } from '../../actions/uiActions';
-import {
- FETCH_END,
- FETCH_ERROR,
- FETCH_START,
-} from '../../actions/fetchActions';
-import { replayStackedCalls } from './stackedCalls';
-import { QueryFunctionParams } from './QueryFunctionParams';
-
-/**
- * In undoable mode, the hook dispatches an optimistic action and executes
- * the success side effects right away. Then it waits for a few seconds to
- * actually call the dataProvider - unless the user dispatches an Undo action.
- *
- * We call that "optimistic" because the hook returns a resolved Promise
- * immediately (although it has an empty value). That only works if the
- * caller reads the result from the Redux store, not from the Promise.
- */
-export const performUndoableQuery = ({
- type,
- payload,
- resource,
- action,
- rest,
- onSuccess,
- onFailure,
- dataProvider,
- dispatch,
- logoutIfAccessDenied,
- allArguments,
-}: QueryFunctionParams): Promise<{}> => {
- dispatch(startOptimisticMode());
- if (window) {
- window.addEventListener('beforeunload', warnBeforeClosingWindow, {
- capture: true,
- });
- }
- dispatch({
- type: action,
- payload,
- meta: { resource, ...rest },
- });
- dispatch({
- type: `${action}_OPTIMISTIC`,
- payload,
- meta: {
- resource,
- fetch: getFetchType(type),
- optimistic: true,
- },
- });
- onSuccess && onSuccess({});
- undoableEventEmitter.once('end', ({ isUndo }) => {
- dispatch(stopOptimisticMode());
- if (isUndo) {
- dispatch(showNotification('ra.notification.canceled'));
- dispatch(refreshView());
- if (window) {
- window.removeEventListener(
- 'beforeunload',
- warnBeforeClosingWindow,
- {
- capture: true,
- }
- );
- }
- return;
- }
- dispatch({
- type: `${action}_LOADING`,
- payload,
- meta: { resource, ...rest },
- });
- dispatch({ type: FETCH_START });
- try {
- dataProvider[type]
- .apply(
- dataProvider,
- typeof resource !== 'undefined'
- ? [resource, payload]
- : allArguments
- )
- .then(response => {
- if (process.env.NODE_ENV !== 'production') {
- validateResponseFormat(response, type);
- }
- dispatch({
- type: `${action}_SUCCESS`,
- payload: response,
- requestPayload: payload,
- meta: {
- ...rest,
- resource,
- fetchResponse: getFetchType(type),
- fetchStatus: FETCH_END,
- },
- });
- dispatch({ type: FETCH_END });
- if (window) {
- window.removeEventListener(
- 'beforeunload',
- warnBeforeClosingWindow,
- {
- capture: true,
- }
- );
- }
- replayStackedCalls();
- })
- .catch(error => {
- if (window) {
- window.removeEventListener(
- 'beforeunload',
- warnBeforeClosingWindow,
- {
- capture: true,
- }
- );
- }
- if (process.env.NODE_ENV !== 'production') {
- console.error(error);
- }
- return logoutIfAccessDenied(error).then(loggedOut => {
- if (loggedOut) return;
- dispatch({
- type: `${action}_FAILURE`,
- error: error.message ? error.message : error,
- payload: error.body ? error.body : null,
- requestPayload: payload,
- meta: {
- ...rest,
- resource,
- fetchResponse: getFetchType(type),
- fetchStatus: FETCH_ERROR,
- },
- });
- dispatch({ type: FETCH_ERROR, error });
- onFailure && onFailure(error);
- });
- });
- } catch (e) {
- if (process.env.NODE_ENV !== 'production') {
- console.error(e);
- }
- throw new Error(
- 'The dataProvider threw an error. It should return a rejected Promise instead.'
- );
- }
- });
- return Promise.resolve({});
-};
-
-// event listener added as window.onbeforeunload when starting optimistic mode, and removed when it ends
-const warnBeforeClosingWindow = event => {
- event.preventDefault(); // standard
- event.returnValue = ''; // Chrome
- return 'Your latest modifications are not yet sent to the server. Are you sure?'; // Old IE
-};
diff --git a/packages/ra-core/src/dataProvider/performQuery/stackedCalls.ts b/packages/ra-core/src/dataProvider/performQuery/stackedCalls.ts
deleted file mode 100644
index 19d453a35be..00000000000
--- a/packages/ra-core/src/dataProvider/performQuery/stackedCalls.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import { doQuery } from './doQuery';
-
-let nbRemainingStackedCalls = 0;
-export const getRemainingStackedCalls = () => nbRemainingStackedCalls;
-
-// List of dataProvider calls emitted while in optimistic mode.
-// These calls get replayed once the dataProvider exits optimistic mode
-const stackedCalls = [];
-export const stackCall = params => {
- stackedCalls.push(params);
- nbRemainingStackedCalls++;
-};
-
-const stackedOptimisticCalls = [];
-export const stackOptimisticCall = params => {
- stackedOptimisticCalls.push(params);
- nbRemainingStackedCalls++;
-};
-
-// Replay calls recorded while in optimistic mode
-export const replayStackedCalls = async () => {
- let clone;
-
- // We must perform any undoable queries first so that the effects of previous undoable
- // queries do not conflict with this one.
-
- // We only handle all side effects queries if there are no more undoable queries
- if (stackedOptimisticCalls.length > 0) {
- clone = [...stackedOptimisticCalls];
- // remove these calls from the list *before* doing them
- // because side effects in the calls can add more calls
- // so we don't want to erase these.
- stackedOptimisticCalls.splice(0, stackedOptimisticCalls.length);
-
- await Promise.all(
- clone.map(params => Promise.resolve(doQuery.call(null, params)))
- );
- // once the calls are finished, decrease the number of remaining calls
- nbRemainingStackedCalls -= clone.length;
- } else {
- clone = [...stackedCalls];
- // remove these calls from the list *before* doing them
- // because side effects in the calls can add more calls
- // so we don't want to erase these.
- stackedCalls.splice(0, stackedCalls.length);
-
- await Promise.all(
- clone.map(params => Promise.resolve(doQuery.call(null, params)))
- );
- // once the calls are finished, decrease the number of remaining calls
- nbRemainingStackedCalls -= clone.length;
- }
-};
diff --git a/packages/ra-core/src/dataProvider/replyWithCache.ts b/packages/ra-core/src/dataProvider/replyWithCache.ts
deleted file mode 100644
index 2488a8bc078..00000000000
--- a/packages/ra-core/src/dataProvider/replyWithCache.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import get from 'lodash/get';
-import {
- GetListParams,
- GetListResult,
- GetOneParams,
- GetOneResult,
- GetManyParams,
- GetManyResult,
-} from '../types';
-
-export const canReplyWithCache = (type, payload, resourceState) => {
- const now = new Date();
- switch (type) {
- case 'getList':
- return (
- get(resourceState, [
- 'list',
- 'cachedRequests',
- JSON.stringify(payload as GetListParams),
- 'validity',
- ]) > now
- );
- case 'getOne':
- return (
- resourceState &&
- resourceState.validity &&
- resourceState.validity[(payload as GetOneParams).id] > now
- );
-
- case 'getMany':
- return (
- resourceState &&
- resourceState.validity &&
- (payload as GetManyParams).ids.every(
- id => resourceState.validity[id] > now
- )
- );
- default:
- return false;
- }
-};
-
-export const getResultFromCache = (type, payload, resourceState) => {
- switch (type) {
- case 'getList': {
- const data = resourceState.data;
- const requestSignature = JSON.stringify(payload);
- const cachedRequest =
- resourceState.list.cachedRequests[requestSignature];
- return {
- data: cachedRequest.ids.map(id => data[id]),
- total: cachedRequest.total,
- } as GetListResult;
- }
- case 'getOne':
- return { data: resourceState.data[payload.id] } as GetOneResult;
- case 'getMany':
- return {
- data: payload.ids.map(id => resourceState.data[id]),
- } as GetManyResult;
- default:
- throw new Error('cannot reply with cache for this method');
- }
-};
diff --git a/packages/ra-core/src/dataProvider/useCreate.ts b/packages/ra-core/src/dataProvider/useCreate.ts
index cbe4f444e67..78a8c5d22ed 100644
--- a/packages/ra-core/src/dataProvider/useCreate.ts
+++ b/packages/ra-core/src/dataProvider/useCreate.ts
@@ -7,7 +7,7 @@ import {
MutateOptions,
} from 'react-query';
-import useDataProvider from './useDataProvider';
+import { useDataProvider } from './useDataProvider';
import { Record, CreateParams } from '../types';
/**
@@ -16,7 +16,7 @@ import { Record, CreateParams } from '../types';
* @param {string} resource
* @param {Params} params The create parameters { data }
* @param {Object} options Options object to pass to the queryClient.
- * May include side effects to be executed upon success or failure, e.g. { onSuccess: { refresh: true } }
+ * May include side effects to be executed upon success or failure, e.g. { onSuccess: () => { refresh(); } }
*
* @typedef Params
* @prop params.data The record to create, e.g. { title: 'hello, world' }
diff --git a/packages/ra-core/src/dataProvider/useDataProvider.spec.js b/packages/ra-core/src/dataProvider/useDataProvider.spec.js
index c1d7b5d5525..b5b631df99f 100644
--- a/packages/ra-core/src/dataProvider/useDataProvider.spec.js
+++ b/packages/ra-core/src/dataProvider/useDataProvider.spec.js
@@ -1,15 +1,10 @@
import * as React from 'react';
import { useState, useEffect } from 'react';
-import { act, fireEvent } from '@testing-library/react';
+import { render, act } from '@testing-library/react';
import expect from 'expect';
-import { renderWithRedux } from 'ra-test';
-import useDataProvider from './useDataProvider';
-import { useUpdate } from './useUpdate';
-import { DataProviderContext } from '../dataProvider';
-import { useRefresh } from '../sideEffect';
-import undoableEventEmitter from './undoableEventEmitter';
-import { QueryClient, QueryClientProvider } from 'react-query';
+import { useDataProvider } from './useDataProvider';
+import { CoreAdminContext } from '../core';
const UseGetOne = () => {
const [data, setData] = useState();
@@ -26,46 +21,16 @@ const UseGetOne = () => {
return loading
;
};
-const UseCustomVerb = ({ onSuccess }) => {
- const [data, setData] = useState();
- const [error, setError] = useState();
- const dataProvider = useDataProvider();
- useEffect(() => {
- dataProvider
- .customVerb({ id: 1 }, ['something'], { onSuccess })
- .then(res => setData(res.data))
- .catch(e => setError(e));
- }, [dataProvider, onSuccess]);
- if (error) return {error.message}
;
- if (data) return {JSON.stringify(data)}
;
- return loading
;
-};
-
-const UseCustomVerbWithStandardSignature = ({ onSuccess }) => {
- const [data, setData] = useState();
- const [error, setError] = useState();
- const dataProvider = useDataProvider();
- useEffect(() => {
- dataProvider
- .customVerb('posts', { id: 1 }, { onSuccess })
- .then(res => setData(res.data))
- .catch(e => setError(e));
- }, [dataProvider, onSuccess]);
- if (error) return {error.message}
;
- if (data) return {JSON.stringify(data)}
;
- return loading
;
-};
-
describe('useDataProvider', () => {
it('should return a way to call the dataProvider', async () => {
const getOne = jest.fn(() =>
Promise.resolve({ data: { id: 1, title: 'foo' } })
);
const dataProvider = { getOne };
- const { queryByTestId } = renderWithRedux(
-
+ const { queryByTestId } = render(
+
-
+
);
expect(queryByTestId('loading')).not.toBeNull();
await act(async () => {
@@ -82,10 +47,10 @@ describe('useDataProvider', () => {
jest.spyOn(console, 'error').mockImplementationOnce(() => {});
const getOne = jest.fn(() => Promise.reject(new Error('foo')));
const dataProvider = { getOne };
- const { queryByTestId } = renderWithRedux(
-
+ const { queryByTestId } = render(
+
-
+
);
expect(queryByTestId('loading')).not.toBeNull();
await act(async () => {
@@ -103,10 +68,10 @@ describe('useDataProvider', () => {
});
const dataProvider = { getOne };
const r = () =>
- renderWithRedux(
-
+ render(
+
-
+
);
expect(r).toThrow(
new Error(
@@ -116,35 +81,28 @@ describe('useDataProvider', () => {
c.mockRestore();
});
- it('should dispatch CUSTOM_FETCH actions by default', async () => {
- const getOne = jest.fn(() => Promise.resolve({ data: { id: 123 } }));
- const dataProvider = { getOne };
- const { dispatch } = renderWithRedux(
-
-
-
- );
- expect(dispatch.mock.calls).toHaveLength(3);
- // waitFor for the dataProvider to return
- await act(async () => {
- await new Promise(resolve => setTimeout(resolve));
- });
- expect(dispatch.mock.calls).toHaveLength(5);
- expect(dispatch.mock.calls[0][0].type).toBe('CUSTOM_FETCH');
- expect(dispatch.mock.calls[1][0].type).toBe('CUSTOM_FETCH_LOADING');
- expect(dispatch.mock.calls[2][0].type).toBe('RA/FETCH_START');
- expect(dispatch.mock.calls[3][0].type).toBe('CUSTOM_FETCH_SUCCESS');
- expect(dispatch.mock.calls[4][0].type).toBe('RA/FETCH_END');
- });
-
it('should call custom verbs with standard signature (resource, payload, options)', async () => {
- const onSuccess = jest.fn();
+ const UseCustomVerbWithStandardSignature = () => {
+ const [data, setData] = useState();
+ const [error, setError] = useState();
+ const dataProvider = useDataProvider();
+ useEffect(() => {
+ dataProvider
+ .customVerb('posts', { id: 1 })
+ .then(res => setData(res.data))
+ .catch(e => setError(e));
+ }, [dataProvider]);
+ if (error) return {error.message}
;
+ if (data)
+ return {JSON.stringify(data)}
;
+ return loading
;
+ };
const customVerb = jest.fn(() => Promise.resolve({ data: null }));
const dataProvider = { customVerb };
- renderWithRedux(
-
-
-
+ render(
+
+
+
);
// waitFor for the dataProvider to return
await act(async () => {
@@ -155,7 +113,7 @@ describe('useDataProvider', () => {
});
it('should accept calls to custom verbs with no arguments', async () => {
- const UseCustomVerbWithNoArgument = ({ onSuccess }) => {
+ const UseCustomVerbWithNoArgument = () => {
const [data, setData] = useState();
const [error, setError] = useState();
const dataProvider = useDataProvider();
@@ -164,7 +122,7 @@ describe('useDataProvider', () => {
.customVerb()
.then(res => setData(res.data))
.catch(e => setError(e));
- }, [dataProvider, onSuccess]);
+ }, [dataProvider]);
if (error) return {error.message}
;
if (data)
return {JSON.stringify(data)}
;
@@ -172,10 +130,10 @@ describe('useDataProvider', () => {
};
const customVerb = jest.fn(() => Promise.resolve({ data: null }));
const dataProvider = { customVerb };
- renderWithRedux(
-
+ render(
+
-
+
);
// waitFor for the dataProvider to return
await act(async () => {
@@ -186,29 +144,27 @@ describe('useDataProvider', () => {
});
it('should accept custom arguments for custom verbs', async () => {
+ const UseCustomVerb = () => {
+ const [data, setData] = useState();
+ const [error, setError] = useState();
+ const dataProvider = useDataProvider();
+ useEffect(() => {
+ dataProvider
+ .customVerb({ id: 1 }, ['something'])
+ .then(res => setData(res.data))
+ .catch(e => setError(e));
+ }, [dataProvider]);
+ if (error) return {error.message}
;
+ if (data)
+ return {JSON.stringify(data)}
;
+ return loading
;
+ };
const customVerb = jest.fn(() => Promise.resolve({ data: null }));
const dataProvider = { customVerb };
- renderWithRedux(
-
+ render(
+
-
- );
- // waitFor for the dataProvider to return
- await act(async () => {
- await new Promise(resolve => setTimeout(resolve));
- });
-
- expect(customVerb).toHaveBeenCalledWith({ id: 1 }, ['something']);
- });
-
- it('should accept custom arguments for custom verbs and allow options', async () => {
- const onSuccess = jest.fn();
- const customVerb = jest.fn(() => Promise.resolve({ data: null }));
- const dataProvider = { customVerb };
- renderWithRedux(
-
-
-
+
);
// waitFor for the dataProvider to return
await act(async () => {
@@ -216,510 +172,5 @@ describe('useDataProvider', () => {
});
expect(customVerb).toHaveBeenCalledWith({ id: 1 }, ['something']);
- expect(onSuccess).toHaveBeenCalledWith({ data: null });
- });
-
- describe('options', () => {
- it('should accept an action option to dispatch a custom action', async () => {
- const UseGetOneWithCustomAction = () => {
- const [data, setData] = useState();
- const dataProvider = useDataProvider();
- useEffect(() => {
- dataProvider
- .getOne('dummy', {}, { action: 'MY_ACTION' })
- .then(res => setData(res.data));
- }, [dataProvider]);
- if (data)
- return {JSON.stringify(data)}
;
- return loading
;
- };
- const getOne = jest.fn(() =>
- Promise.resolve({ data: { id: 123 } })
- );
- const dataProvider = { getOne };
- const { dispatch } = renderWithRedux(
-
-
-
- );
- expect(dispatch.mock.calls).toHaveLength(3);
- // waitFor for the dataProvider to return
- await act(async () => {
- await new Promise(resolve => setTimeout(resolve));
- });
- expect(dispatch.mock.calls).toHaveLength(5);
- expect(dispatch.mock.calls[0][0].type).toBe('MY_ACTION');
- expect(dispatch.mock.calls[1][0].type).toBe('MY_ACTION_LOADING');
- expect(dispatch.mock.calls[2][0].type).toBe('RA/FETCH_START');
- expect(dispatch.mock.calls[3][0].type).toBe('MY_ACTION_SUCCESS');
- expect(dispatch.mock.calls[4][0].type).toBe('RA/FETCH_END');
- });
-
- it('should accept an onSuccess option to execute on success', async () => {
- const onSuccess = jest.fn();
- const UseGetOneWithOnSuccess = () => {
- const [data, setData] = useState();
- const dataProvider = useDataProvider();
- useEffect(() => {
- dataProvider
- .getOne('dummy', {}, { onSuccess })
- .then(res => setData(res.data));
- }, [dataProvider]);
- if (data)
- return {JSON.stringify(data)}
;
- return loading
;
- };
- const getOne = jest.fn(() =>
- Promise.resolve({ data: { id: 1, foo: 'bar' } })
- );
- const dataProvider = { getOne };
- renderWithRedux(
-
-
-
- );
- expect(onSuccess.mock.calls).toHaveLength(0);
- // waitFor for the dataProvider to return
- await act(async () => {
- await new Promise(resolve => setTimeout(resolve));
- });
- expect(onSuccess.mock.calls).toHaveLength(1);
- expect(onSuccess.mock.calls[0][0]).toEqual({
- data: { id: 1, foo: 'bar' },
- });
- });
-
- it('should accept an onFailure option to execute on failure', async () => {
- jest.spyOn(console, 'error').mockImplementationOnce(() => {});
- const onFailure = jest.fn();
- const UseGetOneWithOnFailure = () => {
- const [error, setError] = useState();
- const dataProvider = useDataProvider();
- useEffect(() => {
- dataProvider
- .getOne('dummy', {}, { onFailure })
- .catch(e => setError(e));
- }, [dataProvider]);
- if (error)
- return {error.message}
;
- return loading
;
- };
- const getOne = jest.fn(() => Promise.reject(new Error('foo')));
- const dataProvider = { getOne };
- renderWithRedux(
-
-
-
- );
- expect(onFailure.mock.calls).toHaveLength(0);
- // waitFor for the dataProvider to return
- await act(async () => {
- await new Promise(resolve => setTimeout(resolve));
- });
- expect(onFailure.mock.calls).toHaveLength(1);
- 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}
- setIsEnabled(e => !e)}>
- toggle
-
-
- );
- };
- 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;
- const update = jest.fn(() =>
- new Promise(resolve => {
- resolveUpdate = resolve;
- }).then(() => ({ data: { id: 1, updated: true } }))
- );
- const dataProvider = { update };
- const UpdateButton = () => {
- const [updated, setUpdated] = useState(false);
- const dataProvider = useDataProvider();
- return (
-
- dataProvider.update(
- 'foo',
- {},
- {
- onSuccess: () => {
- setUpdated(true);
- },
- mutationMode: 'pessimistic',
- }
- )
- }
- >
- {updated ? '(updated)' : 'update'}
-
- );
- };
- const { getByText, queryByText } = renderWithRedux(
-
-
- ,
- { admin: { resources: { posts: { data: {}, list: {} } } } }
- );
- // click on the update button
- await act(async () => {
- fireEvent.click(getByText('update'));
- await new Promise(r => setTimeout(r));
- });
- expect(update).toBeCalledTimes(1);
- // make sure the side effect hasn't been applied yet
- expect(queryByText('(updated)')).toBeNull();
- await act(async () => {
- resolveUpdate();
- });
- // side effects should be applied now
- expect(queryByText('(updated)')).not.toBeNull();
- });
-
- it('should not wait for response to dispatch side effects in optimistic mode', async () => {
- let resolveUpdate;
- const update = jest.fn(() =>
- new Promise(resolve => {
- resolveUpdate = resolve;
- }).then(() => ({ data: { id: 1, updated: true } }))
- );
- const dataProvider = { update };
- const UpdateButton = () => {
- const [updated, setUpdated] = useState(false);
- const dataProvider = useDataProvider();
- return (
-
- dataProvider.update(
- 'foo',
- {},
- {
- onSuccess: () => {
- setUpdated(true);
- },
- mutationMode: 'optimistic',
- }
- )
- }
- >
- {updated ? '(updated)' : 'update'}
-
- );
- };
- const { getByText, queryByText } = renderWithRedux(
-
-
- ,
- { admin: { resources: { posts: { data: {}, list: {} } } } }
- );
- // click on the update button
- await act(async () => {
- fireEvent.click(getByText('update'));
- await new Promise(r => setTimeout(r));
- });
- // side effects should be applied now
- expect(queryByText('(updated)')).not.toBeNull();
- expect(update).toBeCalledTimes(1);
- act(() => {
- resolveUpdate();
- });
- });
-
- it('should not wait for response to dispatch side effects in undoable mode', async () => {
- const update = jest.fn({
- apply: () =>
- Promise.resolve({ data: { id: 1, updated: true } }),
- });
- const dataProvider = { update };
- const UpdateButton = () => {
- const [updated, setUpdated] = useState(false);
- const dataProvider = useDataProvider();
- return (
-
- dataProvider.update(
- 'foo',
- {},
- {
- onSuccess: () => {
- setUpdated(true);
- },
- mutationMode: 'undoable',
- }
- )
- }
- >
- {updated ? '(updated)' : 'update'}
-
- );
- };
- const { getByText, queryByText } = renderWithRedux(
-
-
- ,
- { admin: { resources: { posts: { data: {}, list: {} } } } }
- );
- // click on the update button
- await act(async () => {
- fireEvent.click(getByText('update'));
- await new Promise(r => setTimeout(r));
- });
- // side effects should be applied now
- expect(queryByText('(updated)')).not.toBeNull();
- // update shouldn't be called at all
- expect(update).toBeCalledTimes(0);
- act(() => {
- undoableEventEmitter.emit('end', {});
- });
- expect(update).toBeCalledTimes(1);
- });
- });
- });
-
- describe('cache', () => {
- it('should not skip the dataProvider call if there is no cache', async () => {
- const getOne = jest.fn(() => Promise.resolve({ data: { id: 1 } }));
- const dataProvider = { getOne };
- const { rerender } = renderWithRedux(
-
-
- ,
- { admin: { resources: { posts: { data: {}, list: {} } } } }
- );
- // waitFor for the dataProvider to return
- await act(async () => await new Promise(r => setTimeout(r)));
- expect(getOne).toBeCalledTimes(1);
- rerender(
-
-
-
- );
- // waitFor for the dataProvider to return
- await act(async () => await new Promise(r => setTimeout(r)));
- expect(getOne).toBeCalledTimes(2);
- });
-
- // to be revisited once we reimplement caching via react-query
- it.skip('should skip the dataProvider call if there is a valid cache', async () => {
- const getOne = jest.fn(() => {
- const validUntil = new Date();
- validUntil.setTime(validUntil.getTime() + 1000);
- return Promise.resolve({ data: { id: 1 }, validUntil });
- });
- const dataProvider = { getOne };
- const { rerender } = renderWithRedux(
-
-
- ,
- { admin: { resources: { posts: { data: {}, list: {} } } } }
- );
- // waitFor for the dataProvider to return
- await act(async () => await new Promise(r => setTimeout(r)));
- expect(getOne).toBeCalledTimes(1);
- rerender(
-
-
-
- );
- // waitFor for the dataProvider to return
- await act(async () => await new Promise(r => setTimeout(r)));
- expect(getOne).toBeCalledTimes(1);
- });
-
- it('should not skip the dataProvider call if there is an invalid cache', async () => {
- const getOne = jest.fn(() => {
- const validUntil = new Date();
- validUntil.setTime(validUntil.getTime() - 1000);
- return Promise.resolve({ data: { id: 1 }, validUntil });
- });
- const dataProvider = { getOne };
- const { rerender } = renderWithRedux(
-
-
- ,
- { admin: { resources: { posts: { data: {}, list: {} } } } }
- );
- // waitFor for the dataProvider to return
- await act(async () => await new Promise(r => setTimeout(r)));
- expect(getOne).toBeCalledTimes(1);
- rerender(
-
-
-
- );
- // waitFor for the dataProvider to return
- await act(async () => await new Promise(r => setTimeout(r)));
- expect(getOne).toBeCalledTimes(2);
- });
-
- it('should not use the cache after a refresh', async () => {
- const getOne = jest.fn(() => {
- const validUntil = new Date();
- validUntil.setTime(validUntil.getTime() + 1000);
- return Promise.resolve({ data: { id: 1 }, validUntil });
- });
- const dataProvider = { getOne };
- const Refresh = () => {
- const refresh = useRefresh();
- return refresh()}>refresh ;
- };
- const { getByText, rerender } = renderWithRedux(
-
-
-
- ,
- { admin: { resources: { posts: { data: {}, list: {} } } } }
- );
- // waitFor for the dataProvider to return
- await act(async () => await new Promise(r => setTimeout(r)));
- // click on the refresh button
- expect(getOne).toBeCalledTimes(1);
- await act(async () => {
- fireEvent.click(getByText('refresh'));
- await new Promise(r => setTimeout(r));
- });
- rerender(
-
-
-
- );
- // waitFor for the dataProvider to return
- await act(async () => await new Promise(r => setTimeout(r)));
- expect(getOne).toBeCalledTimes(2);
- });
-
- it('should not use the cache after a hard refresh', async () => {
- const getOne = jest.fn(() => {
- const validUntil = new Date();
- validUntil.setTime(validUntil.getTime() + 1000);
- return Promise.resolve({ data: { id: 1 }, validUntil });
- });
- const dataProvider = { getOne };
- const Refresh = () => {
- const refresh = useRefresh();
- return refresh(true)}>refresh ;
- };
- const { getByText, rerender } = renderWithRedux(
-
-
-
- ,
- { admin: { resources: { posts: { data: {}, list: {} } } } }
- );
- // waitFor for the dataProvider to return
- await act(async () => await new Promise(r => setTimeout(r)));
- // click on the refresh button
- expect(getOne).toBeCalledTimes(1);
- await act(async () => {
- fireEvent.click(getByText('refresh'));
- await new Promise(r => setTimeout(r));
- });
- rerender(
-
-
-
- );
- // waitFor for the dataProvider to return
- await act(async () => await new Promise(r => setTimeout(r)));
- expect(getOne).toBeCalledTimes(2);
- });
-
- it('should not use the cache after an update', async () => {
- const getOne = jest.fn(() => {
- const validUntil = new Date();
- validUntil.setTime(validUntil.getTime() + 1000);
- return Promise.resolve({ data: { id: 1 }, validUntil });
- });
- const dataProvider = {
- getOne,
- update: () => Promise.resolve({ data: { id: 1, foo: 'bar' } }),
- };
- const Update = () => {
- const [update] = useUpdate('posts', {
- id: 1,
- data: { foo: 'bar ' },
- });
- return update()}>update ;
- };
- const { getByText, rerender } = renderWithRedux(
-
-
-
-
-
- ,
- { admin: { resources: { posts: { data: {}, list: {} } } } }
- );
- // waitFor for the dataProvider to return
- await act(async () => await new Promise(r => setTimeout(r)));
- expect(getOne).toBeCalledTimes(1);
- // click on the update button
- await act(async () => {
- fireEvent.click(getByText('update'));
- await new Promise(r => setTimeout(r));
- });
- rerender(
-
-
-
- );
- // waitFor for the dataProvider to return
- await act(async () => await new Promise(r => setTimeout(r)));
- expect(getOne).toBeCalledTimes(2);
- });
});
});
diff --git a/packages/ra-core/src/dataProvider/useDataProvider.ts b/packages/ra-core/src/dataProvider/useDataProvider.ts
index 52e598ecf81..f4681e3995a 100644
--- a/packages/ra-core/src/dataProvider/useDataProvider.ts
+++ b/packages/ra-core/src/dataProvider/useDataProvider.ts
@@ -1,27 +1,18 @@
import { useContext, useMemo } from 'react';
-import { Dispatch } from 'redux';
-import { useDispatch, useStore } from 'react-redux';
import DataProviderContext from './DataProviderContext';
import defaultDataProvider from './defaultDataProvider';
-import { ReduxState, DataProvider, DataProviderProxy } from '../types';
+import validateResponseFormat from './validateResponseFormat';
+import { DataProvider } from '../types';
import useLogoutIfAccessDenied from '../auth/useLogoutIfAccessDenied';
-import { getDataProviderCallArguments } from './getDataProviderCallArguments';
-import { doQuery } from './performQuery';
/**
* Hook for getting a dataProvider
*
* Gets a dataProvider object, which behaves just like the real dataProvider
- * (same methods returning a Promise). But it's actually a Proxy object, which
- * dispatches Redux actions along the process. The benefit is that react-admin
- * tracks the loading state when using this hook, and stores results in the
- * Redux store for future use.
- *
- * In addition to the 2 usual parameters of the dataProvider methods (resource,
- * payload), the Proxy supports a third parameter for every call. It's an
- * object literal which may contain side effects, or make the action optimistic
- * (with mutationMode: optimistic) or undoable (with mutationMode: undoable).
+ * (same methods returning a Promise). But it's actually a Proxy object,
+ * which validates the response format, and logs the user out upon error
+ * if authProvider.checkError() rejects.
*
* @return dataProvider
*
@@ -79,36 +70,13 @@ import { doQuery } from './performQuery';
*
* )
* }
- *
- * @example Action customization
- *
- * dataProvider.getOne('users', { id: 123 });
- * // will dispatch the following actions:
- * // - CUSTOM_FETCH
- * // - CUSTOM_FETCH_LOADING
- * // - FETCH_START
- * // - CUSTOM_FETCH_SUCCESS
- * // - FETCH_END
- *
- * dataProvider.getOne('users', { id: 123 }, { action: CRUD_GET_ONE });
- * // will dispatch the following actions:
- * // - CRUD_GET_ONE
- * // - CRUD_GET_ONE_LOADING
- * // - FETCH_START
- * // - CRUD_GET_ONE_SUCCESS
- * // - FETCH_END
*/
-const useDataProvider = <
- TDataProvider extends DataProvider = DataProvider,
- TDataProviderProxy extends DataProviderProxy<
- TDataProvider
- > = DataProviderProxy
->(): TDataProviderProxy => {
- const dispatch = useDispatch() as Dispatch;
+export const useDataProvider = <
+ TDataProvider extends DataProvider = DataProvider
+>(): TDataProvider => {
const dataProvider = ((useContext(DataProviderContext) ||
defaultDataProvider) as unknown) as TDataProvider;
- const store = useStore();
const logoutIfAccessDenied = useLogoutIfAccessDenied();
const dataProviderProxy = useMemo(() => {
@@ -118,74 +86,46 @@ const useDataProvider = <
return;
}
return (...args) => {
- const {
- resource,
- payload,
- allArguments,
- options,
- } = getDataProviderCallArguments(args);
-
const type = name.toString();
- const {
- action = 'CUSTOM_FETCH',
- onSuccess = undefined,
- onFailure = undefined,
- mutationMode = 'pessimistic',
- enabled = true,
- ...rest
- } = options || {};
if (typeof dataProvider[type] !== 'function') {
throw new Error(
`Unknown dataProvider function: ${type}`
);
}
- if (onSuccess && typeof onSuccess !== 'function') {
- throw new Error(
- 'The onSuccess option must be a function'
- );
- }
- if (onFailure && typeof onFailure !== 'function') {
- throw new Error(
- 'The onFailure option must be a function'
- );
- }
- if (mutationMode === 'undoable' && !onSuccess) {
+
+ try {
+ return dataProvider[type]
+ .apply(dataProvider, args)
+ .then(response => {
+ if (process.env.NODE_ENV !== 'production') {
+ validateResponseFormat(response, type);
+ }
+ return response;
+ })
+ .catch(error => {
+ if (process.env.NODE_ENV !== 'production') {
+ console.error(error);
+ }
+ return logoutIfAccessDenied(error).then(
+ loggedOut => {
+ if (loggedOut) return;
+ throw error;
+ }
+ );
+ });
+ } catch (e) {
+ if (process.env.NODE_ENV !== 'production') {
+ console.error(e);
+ }
throw new Error(
- 'You must pass an onSuccess callback calling notify() to use the undoable mode'
+ 'The dataProvider threw an error. It should return a rejected Promise instead.'
);
}
- if (typeof enabled !== 'boolean') {
- throw new Error('The enabled option must be a boolean');
- }
-
- if (enabled === false) {
- return Promise.resolve({});
- }
-
- const params = {
- resource,
- type,
- payload,
- action,
- onFailure,
- onSuccess,
- rest,
- mutationMode,
- // these ones are passed down because of the rules of hooks
- dataProvider,
- store,
- dispatch,
- logoutIfAccessDenied,
- allArguments,
- };
- return doQuery(params);
};
},
});
- }, [dataProvider, dispatch, logoutIfAccessDenied, store]);
+ }, [dataProvider, logoutIfAccessDenied]);
- return (dataProviderProxy as unknown) as TDataProviderProxy;
+ return dataProviderProxy;
};
-
-export default useDataProvider;
diff --git a/packages/ra-core/src/dataProvider/useDelete.ts b/packages/ra-core/src/dataProvider/useDelete.ts
index ee2296ca25f..5ca6b12bac5 100644
--- a/packages/ra-core/src/dataProvider/useDelete.ts
+++ b/packages/ra-core/src/dataProvider/useDelete.ts
@@ -8,7 +8,7 @@ import {
QueryKey,
} from 'react-query';
-import useDataProvider from './useDataProvider';
+import { useDataProvider } from './useDataProvider';
import undoableEventEmitter from './undoableEventEmitter';
import { Record, DeleteParams, MutationMode } from '../types';
@@ -18,7 +18,7 @@ import { Record, DeleteParams, MutationMode } from '../types';
* @param {string} resource
* @param {Params} params The delete parameters { id, previousData }
* @param {Object} options Options object to pass to the queryClient.
- * May include side effects to be executed upon success or failure, e.g. { onSuccess: { refresh: true } }
+ * May include side effects to be executed upon success or failure, e.g. { onSuccess: () => { refresh(); } }
* May include a mutation mode (optimistic/pessimistic/undoable), e.g. { mutationMode: 'undoable' }
*
* @typedef Params
diff --git a/packages/ra-core/src/dataProvider/useDeleteMany.ts b/packages/ra-core/src/dataProvider/useDeleteMany.ts
index b3698e49301..7e25a53aefe 100644
--- a/packages/ra-core/src/dataProvider/useDeleteMany.ts
+++ b/packages/ra-core/src/dataProvider/useDeleteMany.ts
@@ -8,7 +8,7 @@ import {
QueryKey,
} from 'react-query';
-import useDataProvider from './useDataProvider';
+import { useDataProvider } from './useDataProvider';
import undoableEventEmitter from './undoableEventEmitter';
import { Record, DeleteManyParams, MutationMode } from '../types';
@@ -18,7 +18,7 @@ import { Record, DeleteManyParams, MutationMode } from '../types';
* @param {string} resource
* @param {Params} params The delete parameters { ids }
* @param {Object} options Options object to pass to the queryClient.
- * May include side effects to be executed upon success or failure, e.g. { onSuccess: { refresh: true } }
+ * May include side effects to be executed upon success or failure, e.g. { onSuccess: () => { refresh(); } }
* May include a mutation mode (optimistic/pessimistic/undoable), e.g. { mutationMode: 'undoable' }
*
* @typedef Params
diff --git a/packages/ra-core/src/dataProvider/useGetList.ts b/packages/ra-core/src/dataProvider/useGetList.ts
index 3601031fab5..6024793f588 100644
--- a/packages/ra-core/src/dataProvider/useGetList.ts
+++ b/packages/ra-core/src/dataProvider/useGetList.ts
@@ -6,7 +6,7 @@ import {
} from 'react-query';
import { Record, GetListParams } from '../types';
-import useDataProvider from './useDataProvider';
+import { useDataProvider } from './useDataProvider';
/**
* Call the dataProvider.getList() method and return the resolved result
@@ -24,7 +24,7 @@ import useDataProvider from './useDataProvider';
* @param {string} resource The resource name, e.g. 'posts'
* @param {Params} params The getList parameters { pagination, sort, filter }
* @param {Object} options Options object to pass to the queryClient.
- * May include side effects to be executed upon success or failure, e.g. { onSuccess: () => { refresh() } }
+ * May include side effects to be executed upon success or failure, e.g. { onSuccess: () => { refresh(); } }
*
* @typedef Params
* @prop params.pagination The request pagination { page, perPage }, e.g. { page: 1, perPage: 10 }
diff --git a/packages/ra-core/src/dataProvider/useGetMany.ts b/packages/ra-core/src/dataProvider/useGetMany.ts
index c20c1476060..9b7d0c21187 100644
--- a/packages/ra-core/src/dataProvider/useGetMany.ts
+++ b/packages/ra-core/src/dataProvider/useGetMany.ts
@@ -7,7 +7,7 @@ import {
} from 'react-query';
import { Record, GetManyParams } from '../types';
-import useDataProvider from './useDataProvider';
+import { useDataProvider } from './useDataProvider';
/**
* Call the dataProvider.getMany() method and return the resolved result
@@ -25,7 +25,7 @@ import useDataProvider from './useDataProvider';
* @param {string} resource The resource name, e.g. 'posts'
* @param {Params} params The getMany parameters { ids }
* @param {Object} options Options object to pass to the queryClient.
- * May include side effects to be executed upon success or failure, e.g. { onSuccess: () => { refresh() } }
+ * May include side effects to be executed upon success or failure, e.g. { onSuccess: () => { refresh(); } }
*
* @typedef Params
* @prop params.ids The ids to get, e.g. [123, 456, 789]
diff --git a/packages/ra-core/src/dataProvider/useGetManyAggregate.ts b/packages/ra-core/src/dataProvider/useGetManyAggregate.ts
index 4600353cb69..c53f003b66e 100644
--- a/packages/ra-core/src/dataProvider/useGetManyAggregate.ts
+++ b/packages/ra-core/src/dataProvider/useGetManyAggregate.ts
@@ -9,8 +9,8 @@ import {
import union from 'lodash/union';
import { UseGetManyHookValue } from './useGetMany';
-import { Identifier, Record, GetManyParams, DataProviderProxy } from '../types';
-import useDataProvider from './useDataProvider';
+import { Identifier, Record, GetManyParams, DataProvider } from '../types';
+import { useDataProvider } from './useDataProvider';
/**
* Call the dataProvider.getMany() method and return the resolved result
@@ -143,7 +143,7 @@ interface GetManyCallArgs {
ids: Identifier[];
resolve: (data: any[]) => void;
reject: (error?: any) => void;
- dataProvider: DataProviderProxy;
+ dataProvider: DataProvider;
queryClient: QueryClient;
}
diff --git a/packages/ra-core/src/dataProvider/useGetManyReference.ts b/packages/ra-core/src/dataProvider/useGetManyReference.ts
index ce745ea5b26..efc13f92081 100644
--- a/packages/ra-core/src/dataProvider/useGetManyReference.ts
+++ b/packages/ra-core/src/dataProvider/useGetManyReference.ts
@@ -6,7 +6,7 @@ import {
} from 'react-query';
import { Record, GetManyReferenceParams } from '../types';
-import useDataProvider from './useDataProvider';
+import { useDataProvider } from './useDataProvider';
/**
* Call the dataProvider.getManyReference() method and return the resolved result
@@ -24,7 +24,7 @@ import useDataProvider from './useDataProvider';
* @param {string} resource The resource name, e.g. 'posts'
* @param {Params} params The getManyReference parameters { target, id, pagination, sort, filter }
* @param {Object} options Options object to pass to the queryClient.
- * May include side effects to be executed upon success or failure, e.g. { onSuccess: () => { refresh() } }
+ * May include side effects to be executed upon success or failure, e.g. { onSuccess: () => { refresh(); } }
*
* @typedef Params
* @prop params.target The target resource key, e.g. 'post_id'
diff --git a/packages/ra-core/src/dataProvider/useGetOne.ts b/packages/ra-core/src/dataProvider/useGetOne.ts
index df615cbdd82..a7ba81a9cff 100644
--- a/packages/ra-core/src/dataProvider/useGetOne.ts
+++ b/packages/ra-core/src/dataProvider/useGetOne.ts
@@ -1,6 +1,6 @@
import { Record, GetOneParams } from '../types';
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
-import useDataProvider from './useDataProvider';
+import { useDataProvider } from './useDataProvider';
/**
* Call the dataProvider.getOne() method and return the resolved value
diff --git a/packages/ra-core/src/dataProvider/useIsAutomaticRefreshEnabled.ts b/packages/ra-core/src/dataProvider/useIsAutomaticRefreshEnabled.ts
deleted file mode 100644
index c570f99804e..00000000000
--- a/packages/ra-core/src/dataProvider/useIsAutomaticRefreshEnabled.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { useSelector } from 'react-redux';
-import { ReduxState } from '../types';
-
-const useIsAutomaticRefreshEnabled = () => {
- const automaticRefreshEnabled = useSelector(
- state => state.admin.ui.automaticRefreshEnabled
- );
-
- return automaticRefreshEnabled;
-};
-
-export default useIsAutomaticRefreshEnabled;
diff --git a/packages/ra-core/src/loading/useLoading.ts b/packages/ra-core/src/dataProvider/useLoading.ts
similarity index 59%
rename from packages/ra-core/src/loading/useLoading.ts
rename to packages/ra-core/src/dataProvider/useLoading.ts
index c1298bf121e..4f32a444612 100644
--- a/packages/ra-core/src/loading/useLoading.ts
+++ b/packages/ra-core/src/dataProvider/useLoading.ts
@@ -1,5 +1,4 @@
-import { useSelector } from 'react-redux';
-import { ReduxState } from '../types';
+import { useIsFetching, useIsMutating } from 'react-query';
/**
* Get the loading status, i.e. a boolean indicating if at least one request is pending
@@ -15,5 +14,8 @@ import { ReduxState } from '../types';
* return loading ? : ;
* }
*/
-export default () =>
- useSelector((state: ReduxState) => state.admin.loading > 0);
+export const useLoading = () => {
+ const isFetching = useIsFetching();
+ const isMutating = useIsMutating();
+ return isFetching || isMutating;
+};
diff --git a/packages/ra-core/src/dataProvider/useMutation.spec.tsx b/packages/ra-core/src/dataProvider/useMutation.spec.tsx
deleted file mode 100644
index d8f36b87e79..00000000000
--- a/packages/ra-core/src/dataProvider/useMutation.spec.tsx
+++ /dev/null
@@ -1,350 +0,0 @@
-import * as React from 'react';
-import { fireEvent, render, screen, waitFor } from '@testing-library/react';
-import expect from 'expect';
-import { Provider } from 'react-redux';
-
-import { createAdminStore, CoreAdminContext, Resource } from '../core';
-import Mutation from './Mutation';
-import { testDataProvider } from '../dataProvider';
-import { renderWithRedux } from 'ra-test';
-import { DataProviderContext } from '.';
-import useMutation from './useMutation';
-
-describe('useMutation', () => {
- const initialState = {
- admin: {
- resources: { foo: {} },
- },
- };
- const store = createAdminStore({ initialState });
-
- it('should pass a callback to trigger the mutation', () => {
- let callback = null;
- render(
-
-
- {mutate => {
- callback = mutate;
- return Hello
;
- }}
-
-
- );
- expect(callback).toBeInstanceOf(Function);
- });
-
- it('should dispatch a fetch action when the mutation callback is triggered', () => {
- const dataProvider = {
- mytype: jest.fn(() => Promise.resolve({ data: { foo: 'bar' } })),
- };
-
- const myPayload = {};
- const dispatch = jest.spyOn(store, 'dispatch');
- render(
-
-
-
- {mutate => Hello }
-
-
-
- );
- fireEvent.click(screen.getByText('Hello'));
- const action = dispatch.mock.calls[0][0];
- expect(action.type).toEqual('CUSTOM_FETCH');
- expect(action.payload).toEqual(myPayload);
- expect(action.meta.resource).toEqual('myresource');
- dispatch.mockRestore();
- });
-
- it('should use callTimePayload and callTimeOptions', () => {
- const dataProvider = {
- mytype: jest.fn(() => Promise.resolve({ data: { foo: 'bar' } })),
- };
-
- const myPayload = { foo: 1 };
- const dispatch = jest.spyOn(store, 'dispatch');
- render(
-
-
-
- {mutate => (
-
- mutate(
- { payload: { bar: 2 } },
- { meta: 'baz' }
- )
- }
- >
- Hello
-
- )}
-
-
-
- );
- fireEvent.click(screen.getByText('Hello'));
- const action = dispatch.mock.calls[0][0];
- expect(action.payload).toEqual({ foo: 1, bar: 2 });
- expect(action.meta.meta).toEqual('baz');
- dispatch.mockRestore();
- });
-
- it('should use callTimeQuery over definition query', () => {
- const dataProvider = {
- mytype: jest.fn(() => Promise.resolve({ data: { foo: 'bar' } })),
- callTimeType: jest.fn(() =>
- Promise.resolve({ data: { foo: 'bar' } })
- ),
- };
-
- const myPayload = { foo: 1 };
- const dispatch = jest.spyOn(store, 'dispatch');
- render(
-
-
-
- {mutate => (
-
- mutate(
- {
- resource: 'callTimeResource',
- type: 'callTimeType',
- payload: { bar: 2 },
- },
- { meta: 'baz' }
- )
- }
- >
- Hello
-
- )}
-
-
-
- );
- fireEvent.click(screen.getByText('Hello'));
- const action = dispatch.mock.calls[0][0];
- expect(action.payload).toEqual({ foo: 1, bar: 2 });
- expect(action.meta.resource).toEqual('callTimeResource');
- expect(action.meta.meta).toEqual('baz');
- expect(dataProvider.mytype).not.toHaveBeenCalled();
- expect(dataProvider.callTimeType).toHaveBeenCalled();
- dispatch.mockRestore();
- });
-
- it('should update the loading state when the mutation callback is triggered', () => {
- const dataProvider = {
- mytype: jest.fn(() => Promise.resolve({ data: { foo: 'bar' } })),
- };
-
- const myPayload = {};
- render(
-
-
- {(mutate, { loading }) => (
-
- Hello
-
- )}
-
-
- );
- expect(screen.getByText('Hello').className).toEqual('idle');
- fireEvent.click(screen.getByText('Hello'));
- expect(screen.getByText('Hello').className).toEqual('loading');
- });
-
- it('should update the data state after a success response', async () => {
- const dataProvider = {
- mytype: jest.fn(() => Promise.resolve({ data: { foo: 'bar' } })),
- };
-
- const Foo = () => (
-
- {(mutate, { data }) => (
-
- {data ? data.foo : 'no data'}
-
- )}
-
- );
- render(
-
-
-
- );
- const testElement = screen.getByTestId('test');
- expect(testElement.textContent).toBe('no data');
- fireEvent.click(testElement);
- await waitFor(() => {
- expect(testElement.textContent).toEqual('bar');
- });
- });
-
- it('should update the error state after an error response', async () => {
- jest.spyOn(console, 'error').mockImplementationOnce(() => {});
- const dataProvider = {
- mytype: jest.fn(() =>
- Promise.reject({ message: 'provider error' })
- ),
- };
- const Foo = () => (
-
- {(mutate, { error }) => (
-
- {error ? error.message : 'no data'}
-
- )}
-
- );
- render(
-
-
-
- );
- const testElement = screen.getByTestId('test');
- expect(testElement.textContent).toBe('no data');
- fireEvent.click(testElement);
- await waitFor(() => {
- expect(testElement.textContent).toEqual('provider error');
- });
- });
-
- it('should allow custom dataProvider methods without resource', () => {
- const dataProvider = {
- mytype: jest.fn(() => Promise.resolve({ data: { foo: 'bar' } })),
- };
-
- const myPayload = {};
- const dispatch = jest.spyOn(store, 'dispatch');
- render(
-
-
-
- {mutate => Hello }
-
-
-
- );
- fireEvent.click(screen.getByText('Hello'));
- const action = dispatch.mock.calls[0][0];
- expect(action.type).toEqual('CUSTOM_FETCH');
- expect(action.meta.resource).toBeUndefined();
- expect(dataProvider.mytype).toHaveBeenCalledWith(myPayload);
- dispatch.mockRestore();
- });
-
- it('should return a promise to dispatch a fetch action when returnPromise option is set and the mutation callback is triggered', async () => {
- const dataProvider = {
- mytype: jest.fn(() => Promise.resolve({ data: { foo: 'bar' } })),
- };
-
- let promise = null;
- const myPayload = {};
- const dispatch = jest.spyOn(store, 'dispatch');
- render(
-
-
-
- {(mutate, { loading }) => (
- (promise = mutate())}
- >
- Hello
-
- )}
-
-
-
- );
- const buttonElement = screen.getByText('Hello');
- fireEvent.click(buttonElement);
- const action = dispatch.mock.calls[0][0];
- expect(action.type).toEqual('CUSTOM_FETCH');
- expect(action.payload).toEqual(myPayload);
- expect(action.meta.resource).toEqual('myresource');
- await waitFor(() => {
- expect(buttonElement.className).toEqual('idle');
- });
- expect(promise).toBeInstanceOf(Promise);
- const result = await promise;
- expect(result).toMatchObject({ data: { foo: 'bar' } });
- dispatch.mockRestore();
- });
-
- it('should return a response when returnPromise option is set at definition and the query is passed at callTime', async () => {
- const MutationComponent = ({ query = undefined, options, children }) =>
- children(...useMutation(query, options));
- const dataProvider = {
- mytype: jest.fn(() => Promise.resolve({ data: { foo: 'bar' } })),
- };
-
- let response = null;
- const myPayload = {};
- const { getByText, dispatch } = renderWithRedux(
-
-
- {(mutate, { loading }) => (
-
- (response = await mutate({
- type: 'mytype',
- resource: 'myresource',
- payload: myPayload,
- }))
- }
- >
- Hello
-
- )}
-
-
- );
- const buttonElement = getByText('Hello');
- fireEvent.click(buttonElement);
- const action = dispatch.mock.calls[0][0];
- expect(action.type).toEqual('CUSTOM_FETCH');
- expect(action.payload).toEqual(myPayload);
- expect(action.meta.resource).toEqual('myresource');
- await waitFor(() => {
- expect(buttonElement.className).toEqual('idle');
- });
-
- expect(response).toMatchObject({ data: { foo: 'bar' } });
- });
-});
diff --git a/packages/ra-core/src/dataProvider/useMutation.ts b/packages/ra-core/src/dataProvider/useMutation.ts
deleted file mode 100644
index 304ec8f8f05..00000000000
--- a/packages/ra-core/src/dataProvider/useMutation.ts
+++ /dev/null
@@ -1,321 +0,0 @@
-import { useCallback } from 'react';
-import merge from 'lodash/merge';
-
-import { useSafeSetState } from '../util/hooks';
-import { MutationMode, OnSuccess, OnFailure } from '../types';
-import useDataProvider from './useDataProvider';
-
-/**
- * Get a callback to fetch the data provider through Redux, usually for mutations.
- *
- * The request starts when the callback is called.
- *
- * useMutation() parameters can be passed:
- *
- * - at definition time
- *
- * const [mutate] = useMutation(query, options); mutate();
- *
- * - at call time
- *
- * const [mutate] = useMutation(); mutate(query, options);
- *
- * - both, in which case the definition and call time parameters are merged
- *
- * const [mutate] = useMutation(query1, options1); mutate(query2, options2);
- *
- * @param {Object} query
- * @param {string} query.type The method called on the data provider, e.g. 'getList', 'getOne'. Can also be a custom method if the dataProvider supports is.
- * @param {string} query.resource A resource name, e.g. 'posts', 'comments'
- * @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.mutationMode Either 'optimistic', 'pessimistic' or 'undoable'
- * @param {boolean} options.returnPromise Set to true to return the result promise of the mutation
- * @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)
- *
- * @returns A tuple with the mutation callback and the request state. Destructure as [mutate, { data, total, error, loading, loaded }].
- *
- * The return value updates according to the request state:
- *
- * - mount: [mutate, { loading: false, loaded: false }]
- * - mutate called: [mutate, { loading: true, loaded: false }]
- * - success: [mutate, { data: [data from response], total: [total from response], loading: false, loaded: true }]
- * - error: [mutate, { error: [error from response], loading: false, loaded: false }]
- *
- * The mutate function accepts the following arguments
- * - {Object} query
- * - {string} query.type The method called on the data provider, e.g. 'update'
- * - {string} query.resource A resource name, e.g. 'posts', 'comments'
- * - {Object} query.payload The payload object, e.g. { id: 123, data: { isApproved: true } }
- * - {Object} options
- * - {string} options.action Redux action type
- * - {boolean} options.mutationMode Either 'optimistic', 'pessimistic' or 'undoable'
- * - {boolean} options.returnPromise Set to true to return the result promise of the mutation
- * - {Function} options.onSuccess Side effect function to be executed upon success or failure, e.g. { onSuccess: response => refresh() }
- * - {Function} options.onFailure Side effect function to be executed upon failure, e.g. { onFailure: error => notify(error.message) }
- *
- * @example
- *
- * // pass parameters at definition time
- * // use when all parameters are determined at definition time
- * // the mutation callback can be used as an even handler
- * // because Event parameters are ignored
- * import { useMutation } from 'react-admin';
- *
- * const ApproveButton = ({ record }) => {
- * const [approve, { loading }] = useMutation({
- * type: 'update',
- * resource: 'comments',
- * payload: { id: record.id, data: { isApproved: true } }
- * });
- * return ;
- * };
- *
- * @example
- *
- * // pass parameters at call time
- * // use when some parameters are only known at call time
- * import { useMutation } from 'react-admin';
- *
- * const ApproveButton = ({ record }) => {
- * const [mutate, { loading }] = useMutation();
- * const approve = event => mutate({
- * type: 'update',
- * resource: 'comments',
- * payload: {
- * id: event.target.dataset.id,
- * data: { isApproved: true, updatedAt: new Date() }
- * },
- * });
- * return ;
- * };
- *
- * @example
- *
- * // use the second argument to pass options
- * import { useMutation, useNotify, CRUD_UPDATE } from 'react-admin';
- *
- * const ResetStockButton = ({ record }) => {
- * const [mutate, { loading }] = useMutation();
- * const notify = useNotify();
- * const handleClick = () => mutate(
- * {
- * type: 'update',
- * resource: 'items',
- * payload: { id: record.id, data: { stock: 0 } }
- * },
- * {
- * mutationMode: 'undoable',
- * action: CRUD_UPDATE,
- * onSuccess: response => notify('Success !'),
- * onFailure: error => notify('Failure !')
- * }
- * );
- * return ;
- * };
- */
-const useMutation = (
- query?: Mutation,
- options?: MutationOptions
-): UseMutationValue => {
- const [state, setState] = useSafeSetState({
- data: null,
- error: null,
- total: null,
- loading: false,
- loaded: false,
- });
-
- const dataProvider = useDataProvider();
-
- /* eslint-disable react-hooks/exhaustive-deps */
- const mutate = useCallback(
- (
- callTimeQuery?: Partial | Event,
- callTimeOptions?: MutationOptions
- ): void | Promise => {
- const params = mergeDefinitionAndCallTimeParameters(
- query,
- callTimeQuery,
- options,
- callTimeOptions
- );
-
- setState(prevState => ({ ...prevState, loading: true }));
-
- const returnPromise = params.options.returnPromise;
-
- const promise = dataProvider[params.type]
- .apply(
- dataProvider,
- typeof params.resource !== 'undefined'
- ? [params.resource, params.payload, params.options]
- : [params.payload, params.options]
- )
- .then(response => {
- const { data, total } = response;
- setState({
- data,
- error: null,
- loaded: true,
- loading: false,
- total,
- });
- if (returnPromise) {
- return response;
- }
- })
- .catch(errorFromResponse => {
- setState({
- data: null,
- error: errorFromResponse,
- loaded: false,
- loading: false,
- total: null,
- });
- if (returnPromise) {
- throw errorFromResponse;
- }
- });
-
- if (returnPromise) {
- return promise;
- }
- },
- [
- // deep equality, see https://github.com/facebook/react/issues/14476#issuecomment-471199055
- JSON.stringify({ query, options }),
- dataProvider,
- setState,
- ]
- /* eslint-enable react-hooks/exhaustive-deps */
- );
-
- return [mutate, state];
-};
-
-export interface Mutation {
- type: string;
- resource?: string;
- payload?: object;
-}
-
-export interface MutationOptions {
- action?: string;
- returnPromise?: boolean;
- onSuccess?: OnSuccess;
- onFailure?: OnFailure;
- mutationMode?: MutationMode;
-}
-
-export type UseMutationValue = [
- (
- query?: Partial | Event,
- options?: Partial
- ) => void | Promise,
- {
- data?: any;
- total?: number;
- error?: any;
- loading: boolean;
- loaded: boolean;
- }
-];
-
-/**
- * Utility function for merging parameters
- *
- * useMutation() parameters can be passed:
- * - at definition time (e.g. useMutation({ type: 'update', resource: 'posts', payload: { id: 1, data: { title: '' } } }) )
- * - at call time (e.g. [mutate] = useMutation(); mutate({ type: 'update', resource: 'posts', payload: { id: 1, data: { title: '' } } }))
- * - both
- *
- * This function merges the definition time and call time parameters.
- *
- * This is useful because useMutation() is used by higher-level hooks like
- * useCreate() or useUpdate(), and these hooks can be called both ways.
- * So it makes sense to make useMutation() capable of handling both call types
- * as it avoids repetition higher in the hook chain.
- *
- * Also, the call time query may be a DOM Event if the callback is used
- * as an event listener, as in:
- *
- * const UpdateButton = () => {
- * const mutate = useMutation({ type: 'update', resource: 'posts', payload: { id: 1, data: { title: '' } } });
- * return Click me
- * };
- *
- * This usage is accepted, and therefore this function checks if the call time
- * query is an Event, and discards it in that case.
- *
- * @param query {Mutation}
- * @param callTimeQuery {Mutation}
- * @param options {Object}
- * @param callTimeOptions {Object}
- *
- * @return { type, resource, payload, options } The merged parameters
- */
-const mergeDefinitionAndCallTimeParameters = (
- query?: Mutation,
- callTimeQuery?: Partial | Event,
- options?: MutationOptions,
- callTimeOptions?: MutationOptions
-): {
- type: string;
- resource: string;
- payload?: object;
- options: MutationOptions;
-} => {
- if (!query && (!callTimeQuery || callTimeQuery instanceof Event)) {
- throw new Error('Missing query either at definition or at call time');
- }
-
- const event = callTimeQuery as Event;
- if (callTimeQuery instanceof Event || !!event?.preventDefault)
- return {
- type: query.type,
- resource: query.resource,
- payload: query.payload,
- options: sanitizeOptions(options),
- };
-
- if (query) {
- return {
- type: callTimeQuery?.type || query.type,
- resource: callTimeQuery?.resource || query.resource,
- payload: callTimeQuery
- ? merge({}, query.payload, callTimeQuery.payload)
- : query.payload,
- options: callTimeOptions
- ? merge(
- {},
- sanitizeOptions(options),
- sanitizeOptions(callTimeOptions)
- )
- : sanitizeOptions(options),
- };
- }
- return {
- type: callTimeQuery.type,
- resource: callTimeQuery.resource,
- payload: callTimeQuery.payload,
- options: options
- ? merge(
- {},
- sanitizeOptions(options),
- sanitizeOptions(callTimeOptions)
- )
- : sanitizeOptions(callTimeOptions),
- };
-};
-
-const sanitizeOptions = (args?: MutationOptions) =>
- args ? { onSuccess: undefined, ...args } : { onSuccess: undefined };
-
-export default useMutation;
diff --git a/packages/ra-core/src/dataProvider/useQuery.ts b/packages/ra-core/src/dataProvider/useQuery.ts
deleted file mode 100644
index 1a23742df98..00000000000
--- a/packages/ra-core/src/dataProvider/useQuery.ts
+++ /dev/null
@@ -1,149 +0,0 @@
-import { useCallback, useEffect, useState } from 'react';
-
-import { useSafeSetState } from '../util/hooks';
-import { OnSuccess, OnFailure } from '../types';
-import useDataProvider from './useDataProvider';
-import useVersion from '../controller/useVersion';
-import { DataProviderQuery, Refetch } from './useQueryWithStore';
-
-/**
- * Call the data provider on mount
- *
- * The return value updates according to the request state:
- *
- * - start: { loading: true, loaded: false, refetch }
- * - success: { data: [data from response], total: [total from response], loading: false, loaded: true, refetch }
- * - error: { error: [error from response], loading: false, loaded: false, refetch }
- *
- * @param {Object} query
- * @param {string} query.type The method called on the data provider, e.g. 'getList', 'getOne'. Can also be a custom method if the dataProvider supports is.
- * @param {string} query.resource A resource name, e.g. 'posts', 'comments'
- * @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)
- *
- * @returns The current request state. Destructure as { data, total, error, loading, loaded, refetch }.
- *
- * @example
- *
- * import { useQuery } from 'react-admin';
- *
- * const UserProfile = ({ record }) => {
- * const { data, loading, error } = useQuery({
- * type: 'getOne',
- * resource: 'users',
- * payload: { id: record.id }
- * });
- * if (loading) { return ; }
- * if (error) { return ERROR
; }
- * return User {data.username}
;
- * };
- *
- * @example
- *
- * import { useQuery } from 'react-admin';
- *
- * const payload = {
- * pagination: { page: 1, perPage: 10 },
- * sort: { field: 'username', order: 'ASC' },
- * };
- * const UserList = () => {
- * const { data, total, loading, error } = useQuery({
- * type: 'getList',
- * resource: 'users',
- * payload
- * });
- * if (loading) { return ; }
- * if (error) { return ERROR
; }
- * return (
- *
- *
Total users: {total}
- *
- * {data.map(user => {user.username} )}
- *
- *
- * );
- * };
- */
-export const useQuery = (
- query: DataProviderQuery,
- options: UseQueryOptions = { onSuccess: undefined }
-): UseQueryValue => {
- const { type, resource, payload } = query;
- const version = useVersion(); // used to allow force reload
- // used to force a refetch without relying on version
- // which might trigger other queries as well
- const [innerVersion, setInnerVersion] = useState(0);
-
- const refetch = useCallback(() => {
- setInnerVersion(prevInnerVersion => prevInnerVersion + 1);
- }, []);
-
- const requestSignature = JSON.stringify({
- query,
- options,
- version,
- innerVersion,
- });
- const [state, setState] = useSafeSetState({
- data: undefined,
- error: null,
- total: null,
- loading: true,
- loaded: false,
- refetch,
- });
- const dataProvider = useDataProvider();
-
- /* eslint-disable react-hooks/exhaustive-deps */
- useEffect(() => {
- setState(prevState => ({ ...prevState, loading: true }));
-
- dataProvider[type]
- .apply(
- dataProvider,
- typeof resource !== 'undefined'
- ? [resource, payload, options]
- : [payload, options]
- )
- .then(({ data, total }) => {
- setState({
- data,
- total,
- loading: false,
- loaded: true,
- refetch,
- });
- })
- .catch(error => {
- setState({
- error,
- loading: false,
- loaded: false,
- refetch,
- });
- });
- }, [requestSignature, dataProvider, setState]);
- /* eslint-enable react-hooks/exhaustive-deps */
-
- return state;
-};
-
-export interface UseQueryOptions {
- action?: string;
- enabled?: boolean;
- onSuccess?: OnSuccess;
- onFailure?: OnFailure;
-}
-
-export type UseQueryValue = {
- data?: any;
- total?: number;
- error?: any;
- loading: boolean;
- loaded: boolean;
- refetch: Refetch;
-};
diff --git a/packages/ra-core/src/dataProvider/useQueryWithStore.spec.tsx b/packages/ra-core/src/dataProvider/useQueryWithStore.spec.tsx
deleted file mode 100644
index 1810f267890..00000000000
--- a/packages/ra-core/src/dataProvider/useQueryWithStore.spec.tsx
+++ /dev/null
@@ -1,375 +0,0 @@
-import * as React from 'react';
-import { fireEvent, waitFor } from '@testing-library/react';
-import expect from 'expect';
-
-import { renderWithRedux } from 'ra-test';
-import { useQueryWithStore } from './useQueryWithStore';
-import { DataProviderContext } from '../dataProvider';
-
-const UseQueryWithStore = ({
- query = { type: 'getOne', resource: 'posts', payload: { id: 1 } },
- options = {},
- dataSelector = state => state.admin?.resources.posts.data[query.payload.id],
- totalSelector = state => null,
- callback = null,
- ...rest
-}) => {
- const hookValue = useQueryWithStore(
- query,
- options,
- dataSelector,
- totalSelector
- );
- if (callback) callback(hookValue);
- return (
- <>
- hello
- hookValue.refetch()}>refetch
- >
- );
-};
-
-describe('useQueryWithStore', () => {
- it('should not call the dataProvider if options.enabled is set to false and run when it changes to true', async () => {
- const dataProvider = {
- getOne: jest.fn(() =>
- Promise.resolve({
- data: { id: 1, title: 'titleFromDataProvider' },
- })
- ),
- };
- const callback = jest.fn();
- const { rerender } = renderWithRedux(
-
-
- ,
- { admin: { resources: { posts: { data: {} } } } }
- );
- let callArgs = callback.mock.calls[0][0];
- expect(callArgs.data).toBeUndefined();
- expect(callArgs.loading).toEqual(false);
- expect(callArgs.loaded).toEqual(false);
- expect(callArgs.error).toBeNull();
- expect(callArgs.total).toBeNull();
-
- await new Promise(resolve => setImmediate(resolve)); // wait for useEffect
- callArgs = callback.mock.calls[1][0];
- expect(callArgs.loading).toEqual(false);
- expect(callArgs.loaded).toEqual(false);
-
- callback.mockClear();
- rerender(
-
-
- ,
- { admin: { resources: { posts: { data: {} } } } }
- );
- callArgs = callback.mock.calls[0][0];
- expect(callArgs.data).toBeUndefined();
- expect(callArgs.loading).toEqual(false);
- expect(callArgs.loaded).toEqual(false);
- expect(callArgs.error).toBeNull();
- expect(callArgs.total).toBeNull();
-
- callback.mockClear();
- await new Promise(resolve => setImmediate(resolve)); // wait for useEffect
- callArgs = callback.mock.calls[0][0];
- expect(callArgs.data).toBeUndefined();
- expect(callArgs.loading).toEqual(true);
- expect(callArgs.loaded).toEqual(false);
- expect(callArgs.error).toBeNull();
- expect(callArgs.total).toBeNull();
-
- callArgs = callback.mock.calls[1][0];
- expect(callArgs.data).toEqual({
- id: 1,
- title: 'titleFromDataProvider',
- });
- expect(callArgs.loading).toEqual(false);
- expect(callArgs.loaded).toEqual(true);
- expect(callArgs.error).toBeNull();
- expect(callArgs.total).toBeNull();
-
- callback.mockClear();
- rerender(
-
-
- ,
- { admin: { resources: { posts: { data: {} } } } }
- );
- callArgs = callback.mock.calls[0][0];
- expect(callArgs.data).toEqual({
- id: 1,
- title: 'titleFromDataProvider',
- });
- expect(callArgs.loading).toEqual(false);
- expect(callArgs.loaded).toEqual(true);
- expect(callArgs.error).toBeNull();
- expect(callArgs.total).toBeNull();
-
- callback.mockClear();
- await new Promise(resolve => setImmediate(resolve)); // wait for useEffect
- callArgs = callback.mock.calls[0][0];
- expect(callArgs.loading).toEqual(false);
- expect(callArgs.loaded).toEqual(false);
- expect(callArgs.error).toBeNull();
- expect(callArgs.total).toBeNull();
- });
-
- it('should return data from dataProvider', async () => {
- const dataProvider = {
- getOne: jest.fn(() =>
- Promise.resolve({
- data: { id: 1, title: 'titleFromDataProvider' },
- })
- ),
- };
- const callback = jest.fn();
- renderWithRedux(
-
-
- ,
- { admin: { resources: { posts: { data: {} } } } }
- );
- let callArgs = callback.mock.calls[0][0];
- expect(callArgs.data).toBeUndefined();
- expect(callArgs.loading).toEqual(true);
- expect(callArgs.loaded).toEqual(false);
- expect(callArgs.error).toBeNull();
- expect(callArgs.total).toBeNull();
- callback.mockClear();
- await new Promise(resolve => setImmediate(resolve)); // dataProvider Promise returns result on next tick
- callArgs = callback.mock.calls[1][0];
- expect(callArgs.data).toEqual({
- id: 1,
- title: 'titleFromDataProvider',
- });
- expect(callArgs.loading).toEqual(false);
- expect(callArgs.loaded).toEqual(true);
- expect(callArgs.error).toBeNull();
- expect(callArgs.total).toBeNull();
- });
-
- it('should return data from the store first, then data from dataProvider', async () => {
- const dataProvider = {
- getOne: jest.fn(() =>
- Promise.resolve({
- data: { id: 2, title: 'titleFromDataProvider' },
- })
- ),
- };
- const callback = jest.fn();
- renderWithRedux(
-
-
- ,
- {
- admin: {
- resources: {
- posts: {
- data: {
- 2: { id: 2, title: 'titleFromReduxStore' },
- },
- },
- },
- },
- }
- );
- let callArgs = callback.mock.calls[0][0];
- expect(callArgs.data).toEqual({ id: 2, title: 'titleFromReduxStore' });
- expect(callArgs.loading).toEqual(true);
- expect(callArgs.loaded).toEqual(true);
- expect(callArgs.error).toBeNull();
- expect(callArgs.total).toBeNull();
- callback.mockClear();
- await waitFor(() => {
- expect(dataProvider.getOne).toHaveBeenCalled();
- });
- // dataProvider Promise returns result on next tick
- await waitFor(() => {
- callArgs = callback.mock.calls[1][0];
- expect(callArgs.data).toEqual({
- id: 2,
- title: 'titleFromDataProvider',
- });
- expect(callArgs.loading).toEqual(false);
- expect(callArgs.loaded).toEqual(true);
- expect(callArgs.error).toBeNull();
- expect(callArgs.total).toBeNull();
- });
- });
-
- it('should return an error when dataProvider returns a rejected Promise', async () => {
- jest.spyOn(console, 'error').mockImplementationOnce(() => {});
- const dataProvider = {
- getOne: jest.fn(() =>
- Promise.reject({
- message: 'error',
- })
- ),
- };
- const callback = jest.fn();
- renderWithRedux(
-
-
- ,
- { admin: { resources: { posts: { data: {} } } } }
- );
- let callArgs = callback.mock.calls[0][0];
- expect(callArgs.data).toBeUndefined();
- expect(callArgs.loading).toEqual(true);
- expect(callArgs.loaded).toEqual(false);
- expect(callArgs.error).toBeNull();
- expect(callArgs.total).toBeNull();
- callback.mockClear();
- await waitFor(() => {
- expect(dataProvider.getOne).toHaveBeenCalled();
- });
- callArgs = callback.mock.calls[0][0];
- expect(callArgs.data).toBeUndefined();
- expect(callArgs.loading).toEqual(false);
- expect(callArgs.loaded).toEqual(false);
- expect(callArgs.error).toEqual({ message: 'error' });
- expect(callArgs.total).toBeNull();
- });
-
- it('should refetch the dataProvider on refresh', async () => {
- const dataProvider = {
- getOne: jest.fn(() =>
- Promise.resolve({
- data: { id: 3, title: 'titleFromDataProvider' },
- })
- ),
- };
- const { dispatch } = renderWithRedux(
-
-
- ,
- {
- admin: {
- resources: {
- posts: {
- data: {
- 3: { id: 3, title: 'titleFromReduxStore' },
- },
- },
- },
- },
- }
- );
- await waitFor(() => {
- expect(dataProvider.getOne).toBeCalledTimes(1);
- });
- dispatch({ type: 'RA/REFRESH_VIEW' });
- await waitFor(() => {
- expect(dataProvider.getOne).toBeCalledTimes(2);
- });
- });
-
- it('should refetch the dataProvider when refetch is called', async () => {
- const dataProvider = {
- getOne: jest.fn(() =>
- Promise.resolve({
- data: { id: 3, title: 'titleFromDataProvider' },
- })
- ),
- };
- const { getByText } = renderWithRedux(
-
-
- ,
- {
- admin: {
- resources: {
- posts: {
- data: {
- 3: { id: 3, title: 'titleFromReduxStore' },
- },
- },
- },
- },
- }
- );
- await waitFor(() => {
- expect(dataProvider.getOne).toBeCalledTimes(1);
- });
- fireEvent.click(getByText('refetch'));
- await waitFor(() => {
- expect(dataProvider.getOne).toBeCalledTimes(2);
- });
- });
-
- it('should call the dataProvider twice for different requests in the same tick', async () => {
- const dataProvider = {
- getOne: jest.fn(() =>
- Promise.resolve({
- data: { id: 1, title: 'titleFromDataProvider' },
- })
- ),
- };
- renderWithRedux(
-
-
-
- ,
- { admin: { resources: { posts: { data: {} } } } }
- );
- await waitFor(() => {
- expect(dataProvider.getOne).toBeCalledTimes(2);
- });
- });
-
- it('should not call the dataProvider twice for the same request in the same tick', async () => {
- const dataProvider = {
- getOne: jest.fn(() =>
- Promise.resolve({
- data: { id: 1, title: 'titleFromDataProvider' },
- })
- ),
- };
- renderWithRedux(
-
-
-
- ,
- { admin: { resources: { posts: { data: {} } } } }
- );
- await waitFor(() => {
- expect(dataProvider.getOne).toBeCalledTimes(1);
- });
- });
-});
diff --git a/packages/ra-core/src/dataProvider/useQueryWithStore.ts b/packages/ra-core/src/dataProvider/useQueryWithStore.ts
deleted file mode 100644
index 0ba58e09cb3..00000000000
--- a/packages/ra-core/src/dataProvider/useQueryWithStore.ts
+++ /dev/null
@@ -1,255 +0,0 @@
-import { useCallback, useEffect, useRef, useState } from 'react';
-import { useSelector } from 'react-redux';
-import isEqual from 'lodash/isEqual';
-
-import useDataProvider from './useDataProvider';
-import useVersion from '../controller/useVersion';
-import getFetchType from './getFetchType';
-import { useSafeSetState } from '../util/hooks';
-import { ReduxState, OnSuccess, OnFailure, DataProvider } from '../types';
-
-export interface DataProviderQuery {
- type: string;
- resource: string;
- payload: object;
-}
-
-export type Refetch = () => void;
-
-export interface UseQueryWithStoreValue {
- data?: any;
- total?: number;
- error?: any;
- loading: boolean;
- loaded: boolean;
- refetch: Refetch;
-}
-
-export interface QueryOptions {
- onSuccess?: OnSuccess;
- onFailure?: OnFailure;
- action?: string;
- enabled?: boolean;
- [key: string]: any;
-}
-
-type PartialQueryState = {
- error?: any;
- loading: boolean;
- loaded: boolean;
-};
-
-const queriesThisTick: { [key: string]: Promise } = {};
-
-/**
- * Default cache selector. Allows to cache responses by default.
- *
- * By default, custom queries are dispatched as a CUSTOM_QUERY Redux action.
- * The useDataProvider hook dispatches a CUSTOM_QUERY_SUCCESS when the response
- * comes, and the customQueries reducer stores the result in the store.
- * This selector reads the customQueries store and acts as a response cache.
- */
-const defaultDataSelector = query => (state: ReduxState) => {
- const key = JSON.stringify({ ...query, type: getFetchType(query.type) });
- return state.admin.customQueries[key]
- ? state.admin.customQueries[key].data
- : undefined;
-};
-
-const defaultTotalSelector = query => (state: ReduxState) => {
- const key = JSON.stringify({ ...query, type: getFetchType(query.type) });
- return state.admin.customQueries[key]
- ? state.admin.customQueries[key].total
- : null;
-};
-
-const defaultIsDataLoaded = (data: any): boolean => data !== undefined;
-
-/**
- * Fetch the data provider through Redux, return the value from the store.
- *
- * The return value updates according to the request state:
- *
- * - start: { loading: true, loaded: false, refetch }
- * - success: { data: [data from response], total: [total from response], loading: false, loaded: true, refetch }
- * - error: { error: [error from response], loading: false, loaded: false, refetch }
- *
- * This hook will return the cached result when called a second time
- * with the same parameters, until the response arrives.
- *
- * @param {Object} query
- * @param {string} query.type The verb passed to th data provider, e.g. 'getList', 'getOne'
- * @param {string} query.resource A resource name, e.g. 'posts', 'comments'
- * @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.
- * @param {Function} totalSelector Redux selector to get the total (optional, only for LIST queries)
- * @param {Function} isDataLoaded
- *
- * @returns The current request state. Destructure as { data, total, error, loading, loaded, refetch }.
- *
- * @example
- *
- * import { useQueryWithStore } from 'react-admin';
- *
- * const UserProfile = ({ record }) => {
- * const { data, loading, error } = useQueryWithStore(
- * {
- * type: 'getOne',
- * resource: 'users',
- * payload: { id: record.id }
- * },
- * {},
- * state => state.admin.resources.users.data[record.id]
- * );
- * if (loading) { return ; }
- * if (error) { return ERROR
; }
- * return User {data.username}
;
- * };
- */
-export const useQueryWithStore = <
- State extends ReduxState = ReduxState,
- TDataProvider extends DataProvider = DataProvider
->(
- query: DataProviderQuery,
- options: QueryOptions = { action: 'CUSTOM_QUERY' },
- dataSelector: (state: State) => any = defaultDataSelector(query),
- totalSelector: (state: State) => number = defaultTotalSelector(query),
- isDataLoaded: (data: any) => boolean = defaultIsDataLoaded
-): UseQueryWithStoreValue => {
- const { type, resource, payload } = query;
- const version = useVersion(); // used to allow force reload
- // used to force a refetch without relying on version
- // which might trigger other queries as well
- const [innerVersion, setInnerVersion] = useState(0);
- const requestSignature = JSON.stringify({
- query,
- options,
- version,
- innerVersion,
- });
- const requestSignatureRef = useRef(requestSignature);
- const data = useSelector(dataSelector);
- const total = useSelector(totalSelector);
-
- const refetch = useCallback(() => {
- setInnerVersion(prevInnerVersion => prevInnerVersion + 1);
- }, []);
-
- const [state, setState]: [
- UseQueryWithStoreValue,
- (StateResult) => void
- ] = useSafeSetState({
- data,
- total,
- error: null,
- loading: options?.enabled === false ? false : true,
- loaded: options?.enabled === false ? false : isDataLoaded(data),
- refetch,
- });
-
- useEffect(() => {
- if (requestSignatureRef.current !== requestSignature) {
- // request has changed, reset the loading state
- requestSignatureRef.current = requestSignature;
- setState({
- data,
- total,
- error: null,
- loading: options?.enabled === false ? false : true,
- loaded: options?.enabled === false ? false : isDataLoaded(data),
- refetch,
- });
- } else if (!isEqual(state.data, data) || state.total !== total) {
- // the dataProvider response arrived in the Redux store
- if (typeof total !== 'undefined' && isNaN(total)) {
- console.error(
- 'Total from response is not a number. Please check your dataProvider or the API.'
- );
- } else {
- setState(prevState => ({
- ...prevState,
- data,
- total,
- loaded: true,
- loading: false,
- }));
- }
- }
- }, [
- data,
- requestSignature,
- setState,
- state.data,
- state.total,
- total,
- isDataLoaded,
- refetch,
- options.enabled,
- ]);
-
- const dataProvider = useDataProvider();
- useEffect(() => {
- // When several identical queries are issued during the same tick,
- // we only pass one query to the dataProvider.
- // To achieve that, the closure keeps a list of dataProvider promises
- // issued this tick. Before calling the dataProvider, this effect
- // checks if another effect has already issued a similar dataProvider
- // call.
- if (!queriesThisTick.hasOwnProperty(requestSignature)) {
- queriesThisTick[requestSignature] = new Promise(
- resolve => {
- dataProvider[type](resource, payload, options)
- // @ts-ignore
- .then(() => {
- // We don't care about the dataProvider response here, because
- // it was already passed to SUCCESS reducers by the dataProvider
- // hook, and the result is available from the Redux store
- // through the data and total selectors.
- // In addition, if the query is optimistic, the response
- // will be empty, so it should not be used at all.
- if (
- requestSignature !== requestSignatureRef.current
- ) {
- resolve(undefined);
- }
-
- resolve({
- error: null,
- loading: false,
- loaded:
- options?.enabled === false ? false : true,
- });
- })
- .catch(error => {
- if (
- requestSignature !== requestSignatureRef.current
- ) {
- resolve(undefined);
- }
- resolve({
- error,
- loading: false,
- loaded: false,
- });
- });
- }
- );
- // cleanup the list on next tick
- setTimeout(() => {
- delete queriesThisTick[requestSignature];
- }, 0);
- }
- (async () => {
- const newState = await queriesThisTick[requestSignature];
- if (newState) setState(state => ({ ...state, ...newState }));
- })();
- // deep equality, see https://github.com/facebook/react/issues/14476#issuecomment-471199055
- }, [requestSignature]); // eslint-disable-line
-
- return state;
-};
diff --git a/packages/ra-core/src/dataProvider/useRefresh.ts b/packages/ra-core/src/dataProvider/useRefresh.ts
new file mode 100644
index 00000000000..bf8e9309692
--- /dev/null
+++ b/packages/ra-core/src/dataProvider/useRefresh.ts
@@ -0,0 +1,22 @@
+import { useCallback } from 'react';
+import { useQueryClient } from 'react-query';
+
+/**
+ * Hook for triggering a page refresh. Returns a callback function.
+ *
+ * The callback invalidates all queries and refetches the active ones.
+ * Any component depending on react-query data will be re-rendered.
+ *
+ * @example
+ *
+ * const refresh = useRefresh();
+ * const handleClick = () => {
+ * refresh();
+ * };
+ */
+export const useRefresh = () => {
+ const queryClient = useQueryClient();
+ return useCallback(() => {
+ queryClient.invalidateQueries();
+ }, [queryClient]);
+};
diff --git a/packages/ra-core/src/dataProvider/useRefreshWhenVisible.ts b/packages/ra-core/src/dataProvider/useRefreshWhenVisible.ts
deleted file mode 100644
index e65a4a0c60b..00000000000
--- a/packages/ra-core/src/dataProvider/useRefreshWhenVisible.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import { useEffect } from 'react';
-import { useRefresh } from '../sideEffect';
-import useIsAutomaticRefreshEnabled from './useIsAutomaticRefreshEnabled';
-
-/**
- * Trigger a refresh of the page when the page comes back from background after a certain delay
- *
- * @param {number} delay Delay in milliseconds since the time the page was hidden. Defaults to 5 minutes.
- */
-const useRefreshWhenVisible = (delay = 1000 * 60 * 5) => {
- const refresh = useRefresh();
- const automaticRefreshEnabled = useIsAutomaticRefreshEnabled();
-
- useEffect(() => {
- if (typeof document === 'undefined') return;
- let lastHiddenTime;
- const handleVisibilityChange = () => {
- if (!automaticRefreshEnabled) {
- return;
- }
- if (document.hidden) {
- // tab goes hidden
- lastHiddenTime = Date.now();
- } else {
- // tab goes visible
- if (Date.now() - lastHiddenTime > delay) {
- refresh();
- }
- lastHiddenTime = null;
- }
- };
- document.addEventListener('visibilitychange', handleVisibilityChange, {
- capture: true,
- });
- return () =>
- document.removeEventListener(
- 'visibilitychange',
- handleVisibilityChange,
- { capture: true }
- );
- }, [automaticRefreshEnabled, delay, refresh]);
-};
-
-export default useRefreshWhenVisible;
diff --git a/packages/ra-core/src/dataProvider/useUpdate.ts b/packages/ra-core/src/dataProvider/useUpdate.ts
index 7b437fe8781..dd755135c2b 100644
--- a/packages/ra-core/src/dataProvider/useUpdate.ts
+++ b/packages/ra-core/src/dataProvider/useUpdate.ts
@@ -8,7 +8,7 @@ import {
QueryKey,
} from 'react-query';
-import useDataProvider from './useDataProvider';
+import { useDataProvider } from './useDataProvider';
import undoableEventEmitter from './undoableEventEmitter';
import { Record, UpdateParams, MutationMode } from '../types';
@@ -18,7 +18,7 @@ import { Record, UpdateParams, MutationMode } from '../types';
* @param {string} resource
* @param {Params} params The update parameters { id, data, previousData }
* @param {Object} options Options object to pass to the queryClient.
- * May include side effects to be executed upon success or failure, e.g. { onSuccess: { refresh: true } }
+ * May include side effects to be executed upon success or failure, e.g. { onSuccess: () => { refresh(); } }
* May include a mutation mode (optimistic/pessimistic/undoable), e.g. { mutationMode: 'undoable' }
*
* @typedef Params
diff --git a/packages/ra-core/src/dataProvider/useUpdateMany.ts b/packages/ra-core/src/dataProvider/useUpdateMany.ts
index 16b6ca6a95e..8cf350b3cdb 100644
--- a/packages/ra-core/src/dataProvider/useUpdateMany.ts
+++ b/packages/ra-core/src/dataProvider/useUpdateMany.ts
@@ -8,7 +8,7 @@ import {
QueryKey,
} from 'react-query';
-import useDataProvider from './useDataProvider';
+import { useDataProvider } from './useDataProvider';
import undoableEventEmitter from './undoableEventEmitter';
import { Record, UpdateManyParams, MutationMode } from '../types';
import { Identifier } from '..';
@@ -19,7 +19,7 @@ import { Identifier } from '..';
* @param {string} resource
* @param {Params} params The updateMany parameters { ids, data }
* @param {Object} options Options object to pass to the queryClient.
- * May include side effects to be executed upon success or failure, e.g. { onSuccess: { refresh: true } }
+ * May include side effects to be executed upon success or failure, e.g. { onSuccess: () => { refresh(); } }
* May include a mutation mode (optimistic/pessimistic/undoable), e.g. { mutationMode: 'undoable' }
*
* @typedef Params
@@ -62,7 +62,7 @@ import { Identifier } from '..';
* const BulkResetViewsButton = ({ selectedIds }) => {
* const [updateMany, { isLoading, error }] = useUpdateMany('posts', { ids: selectedIds, data: { views: 0 } });
* if (error) { return ERROR
; }
- * return Reset views ;
+ * return updateMany()}>Reset views ;
* };
*/
export const useUpdateMany = (
diff --git a/packages/ra-core/src/dataProvider/withDataProvider.tsx b/packages/ra-core/src/dataProvider/withDataProvider.tsx
index 0bc3c6be0b3..a51530b2bb0 100644
--- a/packages/ra-core/src/dataProvider/withDataProvider.tsx
+++ b/packages/ra-core/src/dataProvider/withDataProvider.tsx
@@ -1,7 +1,7 @@
import * as React from 'react';
import { DataProvider } from '../types';
-import useDataProvider from './useDataProvider';
+import { useDataProvider } from './useDataProvider';
export interface DataProviderProps {
dataProvider: DataProvider;
diff --git a/packages/ra-core/src/export/fetchRelatedRecords.ts b/packages/ra-core/src/export/fetchRelatedRecords.ts
index 7c730d30b6f..067ca49af8d 100644
--- a/packages/ra-core/src/export/fetchRelatedRecords.ts
+++ b/packages/ra-core/src/export/fetchRelatedRecords.ts
@@ -1,4 +1,4 @@
-import { Record, Identifier, DataProviderProxy } from '../types';
+import { Record, Identifier, DataProvider } from '../types';
/**
* Helper function for calling the dataProvider.getMany() method,
@@ -12,7 +12,7 @@ import { Record, Identifier, DataProviderProxy } from '../types';
* }))
* );
*/
-const fetchRelatedRecords = (dataProvider: DataProviderProxy) => (
+const fetchRelatedRecords = (dataProvider: DataProvider) => (
data,
field,
resource
diff --git a/packages/ra-core/src/form/FormWithRedirect.spec.tsx b/packages/ra-core/src/form/FormWithRedirect.spec.tsx
index 5a5cae748d1..5ffd29f1b0e 100644
--- a/packages/ra-core/src/form/FormWithRedirect.spec.tsx
+++ b/packages/ra-core/src/form/FormWithRedirect.spec.tsx
@@ -1,9 +1,10 @@
import * as React from 'react';
+import { screen, render, waitFor } from '@testing-library/react';
-import { renderWithRedux } from 'ra-test';
+import { CoreAdminContext } from '../core';
+import { testDataProvider } from '../dataProvider';
import FormWithRedirect from './FormWithRedirect';
import useInput from './useInput';
-import { waitFor } from '@testing-library/dom';
describe('FormWithRedirect', () => {
const Input = props => {
@@ -16,31 +17,38 @@ describe('FormWithRedirect', () => {
const renderProp = jest.fn(() => (
));
- const { getByDisplayValue, rerender } = renderWithRedux(
-
+ const { rerender } = render(
+
+
+
);
- expect(renderProp.mock.calls[0][0].pristine).toEqual(true);
- expect(getByDisplayValue('Bar')).not.toBeNull();
+ expect(screen.getByDisplayValue('Bar')).not.toBeNull();
+ expect(renderProp).toHaveBeenLastCalledWith(
+ expect.objectContaining({ pristine: true })
+ );
rerender(
-
+
+
+
);
- expect(renderProp.mock.calls[1][0].pristine).toEqual(true);
+ expect(screen.getByDisplayValue('Foo')).not.toBeNull();
+ expect(renderProp).toHaveBeenLastCalledWith(
+ expect.objectContaining({ pristine: true })
+ );
expect(renderProp).toHaveBeenCalledTimes(2);
});
@@ -48,19 +56,22 @@ describe('FormWithRedirect', () => {
const renderProp = jest.fn(() => (
));
- const { getByDisplayValue } = renderWithRedux(
-
- );
-
- expect(renderProp.mock.calls[0][0].pristine).toEqual(true);
- expect(getByDisplayValue('Bar')).not.toBeNull();
+ render(
+
+
+
+ );
+
+ expect(screen.getByDisplayValue('Bar')).not.toBeNull();
+ expect(renderProp).toHaveBeenLastCalledWith(
+ expect.objectContaining({ pristine: true })
+ );
expect(renderProp).toHaveBeenCalledTimes(1);
});
@@ -68,20 +79,23 @@ describe('FormWithRedirect', () => {
const renderProp = jest.fn(() => (
));
- const { getByDisplayValue } = renderWithRedux(
-
- );
-
- expect(renderProp.mock.calls[1][0].pristine).toEqual(false);
- expect(getByDisplayValue('Bar')).not.toBeNull();
- // 4 times because the first initialization with an empty value
+ render(
+
+
+
+ );
+
+ expect(screen.getByDisplayValue('Bar')).not.toBeNull();
+ expect(renderProp).toHaveBeenLastCalledWith(
+ expect.objectContaining({ pristine: false })
+ );
+ // twice because the first initialization with an empty value
// triggers a change on the input which has a defaultValue
// This is expected and identical to what FinalForm does (https://final-form.org/docs/final-form/types/FieldConfig#defaultvalue)
expect(renderProp).toHaveBeenCalledTimes(2);
@@ -91,33 +105,43 @@ describe('FormWithRedirect', () => {
const renderProp = jest.fn(() => (
));
- const { getByDisplayValue, rerender } = renderWithRedux(
-
+ const { rerender } = render(
+
+
+
);
- expect(renderProp.mock.calls[0][0].pristine).toEqual(true);
- expect(getByDisplayValue('Foo')).not.toBeNull();
+ expect(screen.getByDisplayValue('Foo')).not.toBeNull();
+ expect(renderProp).toHaveBeenLastCalledWith(
+ expect.objectContaining({ pristine: true })
+ );
rerender(
-
- );
-
- expect(renderProp.mock.calls[1][0].pristine).toEqual(true);
- expect(getByDisplayValue('Foo')).not.toBeNull();
+
+
+
+ );
+
+ expect(screen.getByDisplayValue('Foo')).not.toBeNull();
+ expect(renderProp).toHaveBeenLastCalledWith(
+ expect.objectContaining({ pristine: true })
+ );
expect(renderProp).toHaveBeenCalledTimes(2);
});
@@ -125,75 +149,85 @@ describe('FormWithRedirect', () => {
const renderProp = jest.fn(() => (
));
- const { getByDisplayValue, rerender } = renderWithRedux(
-
+ const { rerender } = render(
+
+
+
);
- expect(renderProp.mock.calls[0][0].pristine).toEqual(true);
- expect(getByDisplayValue('Foo')).not.toBeNull();
+ expect(screen.getByDisplayValue('Foo')).not.toBeNull();
+ expect(renderProp).toHaveBeenLastCalledWith(
+ expect.objectContaining({ pristine: true })
+ );
rerender(
-
+
+
+
);
+ expect(screen.getByDisplayValue('Bar')).not.toBeNull();
+ expect(renderProp).toHaveBeenLastCalledWith(
+ expect.objectContaining({ pristine: false })
+ );
expect(renderProp).toHaveBeenCalledTimes(3);
- expect(renderProp.mock.calls[2][0].pristine).toEqual(false);
- expect(getByDisplayValue('Bar')).not.toBeNull();
});
it('Does not make the form dirty when reinitialized from a different record with a missing field and this field has an initialValue', async () => {
const renderProp = jest.fn(() => (
));
- const { getByDisplayValue, rerender } = renderWithRedux(
-
+ const { rerender } = render(
+
+
+
);
- expect(renderProp.mock.calls[0][0].pristine).toEqual(true);
- expect(getByDisplayValue('Foo')).not.toBeNull();
+ expect(screen.getByDisplayValue('Foo')).not.toBeNull();
+ expect(renderProp).toHaveBeenLastCalledWith(
+ expect.objectContaining({ pristine: true })
+ );
rerender(
-
+
+
+
);
await waitFor(() => {
- expect(getByDisplayValue('Bar')).not.toBeNull();
+ expect(screen.getByDisplayValue('Bar')).not.toBeNull();
});
expect(
renderProp.mock.calls[renderProp.mock.calls.length - 1][0].pristine
diff --git a/packages/ra-core/src/form/FormWithRedirect.tsx b/packages/ra-core/src/form/FormWithRedirect.tsx
index eaac0073e1a..0c6ed60f0fc 100644
--- a/packages/ra-core/src/form/FormWithRedirect.tsx
+++ b/packages/ra-core/src/form/FormWithRedirect.tsx
@@ -1,8 +1,7 @@
import * as React from 'react';
-import { useRef, useCallback, useEffect, useMemo } from 'react';
+import { useRef, useCallback, useMemo } from 'react';
import { Form, FormProps, FormRenderProps } from 'react-final-form';
import arrayMutators from 'final-form-arrays';
-import { useDispatch } from 'react-redux';
import useResetSubmitErrors from './useResetSubmitErrors';
import sanitizeEmptyValues from './sanitizeEmptyValues';
@@ -15,7 +14,6 @@ import {
OnFailure,
} from '../types';
import { RedirectionSideEffect } from '../sideEffect';
-import { setAutomaticRefresh } from '../actions';
import { useRecordContext, OptionalRecordContextProvider } from '../controller';
import { FormContextProvider } from './FormContextProvider';
import submitErrorsMutators from './submitErrorsMutators';
@@ -63,7 +61,6 @@ const FormWithRedirect = ({
subscription = defaultSubscription,
validate,
validateOnBlur,
- version,
warnWhenUnsavedChanges,
sanitizeEmptyValues: shouldSanitizeEmptyValues = true,
...props
@@ -199,7 +196,7 @@ const FormWithRedirect = ({
diff --git a/packages/ra-ui-materialui/src/detail/Edit.tsx b/packages/ra-ui-materialui/src/detail/Edit.tsx
index 5e85b21a3ad..560f6b988f7 100644
--- a/packages/ra-ui-materialui/src/detail/Edit.tsx
+++ b/packages/ra-ui-materialui/src/detail/Edit.tsx
@@ -25,7 +25,6 @@ import { EditView } from './EditView';
* - actions
* - aside
* - component
- * - successMessage
* - title
* - mutationMode
*
@@ -91,7 +90,6 @@ Edit.propTypes = {
queryOptions: PropTypes.object,
mutationOptions: PropTypes.object,
resource: PropTypes.string,
- successMessage: PropTypes.string,
title: PropTypes.node,
transform: PropTypes.func,
};
diff --git a/packages/ra-ui-materialui/src/detail/EditView.tsx b/packages/ra-ui-materialui/src/detail/EditView.tsx
index 1bfd881d955..b3bd7c33132 100644
--- a/packages/ra-ui-materialui/src/detail/EditView.tsx
+++ b/packages/ra-ui-materialui/src/detail/EditView.tsx
@@ -35,7 +35,6 @@ export const EditView = (props: EditViewProps) => {
resource,
save,
saving,
- version,
} = useEditContext(props);
const finalActions =
@@ -86,7 +85,6 @@ export const EditView = (props: EditViewProps) => {
: children.props.save,
saving,
mutationMode,
- version,
})
) : (
@@ -96,7 +94,6 @@ export const EditView = (props: EditViewProps) => {
React.cloneElement(aside, {
record,
resource,
- version,
save:
typeof children.props.save === 'undefined'
? save
@@ -133,7 +130,6 @@ EditView.propTypes = {
resource: PropTypes.string,
save: PropTypes.func,
title: PropTypes.node,
- version: PropTypes.number,
onSuccess: PropTypes.func,
onFailure: PropTypes.func,
setOnSuccess: PropTypes.func,
@@ -169,7 +165,6 @@ const sanitizeRestProps = ({
setOnFailure = null,
setOnSuccess = null,
setTransform = null,
- successMessage = null,
transform = null,
transformRef = null,
...rest
diff --git a/packages/ra-ui-materialui/src/detail/ShowView.tsx b/packages/ra-ui-materialui/src/detail/ShowView.tsx
index 849080f7daf..6e043ef9d3e 100644
--- a/packages/ra-ui-materialui/src/detail/ShowView.tsx
+++ b/packages/ra-ui-materialui/src/detail/ShowView.tsx
@@ -23,7 +23,7 @@ export const ShowView = (props: ShowViewProps) => {
...rest
} = props;
- const { defaultTitle, record, version } = useShowContext(props);
+ const { defaultTitle, record } = useShowContext(props);
const { hasEdit } = useResourceDefinition(props);
const finalActions =
@@ -35,7 +35,6 @@ export const ShowView = (props: ShowViewProps) => {
return (
{
const { className, ...rest } = props;
- useRefreshWhenVisible();
- const loading = useSelector(state => state.admin.loading > 0);
+ const loading = useLoading();
const theme = useTheme();
return (
diff --git a/packages/ra-ui-materialui/src/list/ListView.tsx b/packages/ra-ui-materialui/src/list/ListView.tsx
index d8918d64c32..ab490204afe 100644
--- a/packages/ra-ui-materialui/src/list/ListView.tsx
+++ b/packages/ra-ui-materialui/src/list/ListView.tsx
@@ -135,7 +135,6 @@ ListView.propTypes = {
showFilter: PropTypes.func,
title: TitlePropType,
total: PropTypes.number,
- version: PropTypes.number,
};
export interface ListViewProps {
diff --git a/packages/ra-ui-materialui/src/list/SingleFieldList.tsx b/packages/ra-ui-materialui/src/list/SingleFieldList.tsx
index 51af43a1551..e3f6d24ce5c 100644
--- a/packages/ra-ui-materialui/src/list/SingleFieldList.tsx
+++ b/packages/ra-ui-materialui/src/list/SingleFieldList.tsx
@@ -15,8 +15,6 @@ import {
useListContext,
useResourceContext,
Record,
- RecordMap,
- Identifier,
RecordContextProvider,
ComponentPropType,
} from 'ra-core';
@@ -133,8 +131,8 @@ export interface SingleFieldListProps
children: React.ReactElement;
// can be injected when using the component without context
basePath?: string;
- data?: RecordMap;
- ids?: Identifier[];
+ data?: RecordType[];
+ total?: number;
loaded?: boolean;
}
diff --git a/packages/ra-ui-materialui/src/list/datagrid/Datagrid.tsx b/packages/ra-ui-materialui/src/list/datagrid/Datagrid.tsx
index d0f3ad8a3be..fef54162f78 100644
--- a/packages/ra-ui-materialui/src/list/datagrid/Datagrid.tsx
+++ b/packages/ra-ui-materialui/src/list/datagrid/Datagrid.tsx
@@ -309,7 +309,6 @@ Datagrid.propTypes = {
selectedIds: PropTypes.arrayOf(PropTypes.any),
setSort: PropTypes.func,
total: PropTypes.number,
- version: PropTypes.number,
isRowSelectable: PropTypes.func,
isRowExpandable: PropTypes.func,
};