diff --git a/docs/Actions.md b/docs/Actions.md index 07b4365b0ec..a701b6843f0 100644 --- a/docs/Actions.md +++ b/docs/Actions.md @@ -30,6 +30,21 @@ Refer to [the `useDataProvider` hook documentation](./useDataProvider.md) for mo 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. +The query hooks execute on mount. They return an object with the following properties: `{ data, isLoading, error }`. Query hooks are: + +* [`useGetList`](./useGetList.md) +* [`useGetOne`](./useGetOne.md) +* [`useGetMany`](./useGetMany.md) +* [`useGetManyReference`](./useGetManyReference.md) + +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: + +* [`useCreate`](./useCreate.md) +* [`useUpdate`](./useUpdate.md) +* [`useUpdateMany`](./useUpdateMany.md) +* [`useDelete`](./useDelete.md) +* [`useDeleteMany`](./useDeleteMany.md) + Their signature is the same as the related dataProvider method, e.g.: ```jsx @@ -59,29 +74,7 @@ const UserProfile = ({ userId }) => { }; ``` -**Tip**: If you use TypeScript, you can specify the record type for more type safety: - -```jsx -const { data, isLoading } = useGetOne('products', { id: 123 }); -// \- type of data is Product -``` - -The query hooks execute on mount. They return an object with the following properties: `{ data, isLoading, error }`. Query hooks are: - -* [`useGetList`](./useGetList.md) -* [`useGetOne`](./useGetOne.md) -* [`useGetMany`](./useGetMany.md) -* [`useGetManyReference`](./useGetManyReference.md) - -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: - -* [`useCreate`](./useCreate.md) -* [`useUpdate`](./useUpdate.md) -* [`useUpdateMany`](./useUpdateMany.md) -* [`useDelete`](./useDelete.md) -* [`useDeleteMany`](./useDeleteMany.md) - -For instance, here is an example using `useUpdate()`: +Here is another example, using `useUpdate()`: ```jsx import * as React from 'react'; @@ -104,6 +97,13 @@ const { data: user, isLoading, error } = useGetOne( ); ``` +**Tip**: If you use TypeScript, you can specify the record type for more type safety: + +```jsx +const { data, isLoading } = useGetOne('products', { id: 123 }); +// \- type of data is Product +``` + ## `meta` Parameter All Data Provider methods accept a `meta` parameter. React-admin doesn't set this parameter by default in its queries, but it's a good way to pass special arguments or metadata to an API call. @@ -227,12 +227,12 @@ const dataProvider = { getList: /* ... */, getOne: /* ... */, getMany: /* ... */, - getManyReference /* ... */, + getManyReference: /* ... */, create: /* ... */, update: /* ... */, - updateMany /* ... */, + updateMany: /* ... */, delete: /* ... */, - deleteMany /* ... */, + deleteMany: /* ... */, banUser: (userId) => { return fetch(`/api/user/${userId}/ban`, { method: 'POST' }) .then(response => response.json()); diff --git a/docs/Admin.md b/docs/Admin.md index 1e3bceeedcc..75e8924671b 100644 --- a/docs/Admin.md +++ b/docs/Admin.md @@ -200,7 +200,7 @@ const dataProvider = { Check the [Writing a Data Provider](./DataProviderWriting.md) chapter for detailed instructions on how to write a data provider for your API. -The `dataProvider` is also the ideal place to add custom HTTP headers, handle file uploads, map resource names to API endpoints, pass credentials to the API, put business logic, reformat API errors, etc. Check [the Data Provider documentation](./DataProviderIntroduction.md) for more details. +The `dataProvider` is also the ideal place to add custom HTTP headers, handle file uploads, map resource names to API endpoints, pass credentials to the API, put business logic, reformat API errors, etc. Check [the Data Provider documentation](./DataProviders.md) for more details. ## `children` diff --git a/docs/DataProviderIntroduction.md b/docs/DataProviderIntroduction.md deleted file mode 100644 index 2eba33348b2..00000000000 --- a/docs/DataProviderIntroduction.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -layout: default -title: "Data Fetching" ---- - -# Data Fetching - -Whenever react-admin needs to communicate with your APIs, it does it through an object called the `dataProvider`. The `dataProvider` exposes a predefined interface that allows react-admin to query any API in a normalized way. - -For instance, to query the API for a single record, react-admin calls `dataProvider.getOne()`: - -```js -dataProvider - .getOne('posts', { id: 123 }) - .then(response => { - console.log(response.data); // { id: 123, title: "hello, world" } - }); -``` - -It's the Data Provider's job to turn these method calls into HTTP requests, and transform the HTTP responses to the data format expected by react-admin. In technical terms, a Data Provider is an *adapter* for an API. - -![Data Provider architecture](./img/data-provider.png) - -Thanks to this adapter system, react-admin can communicate with any API, whether it uses REST, GraphQL, RPC, or even SOAP, regardless of the dialect it uses. Check out the [list of supported backends](./DataProviderList.md) to pick an open-source package for your API. - -You can also [Write your own Data Provider](./DataProviderWriting.md) so that it fits the particularities of your backend(s). Data Providers can use `fetch`, `axios`, `apollo-client`, or any other library to communicate with APIs. The Data Provider is also the ideal place to add custom HTTP headers, authentication, etc. - -A Data Provider must have the following methods: - -```jsx -const dataProvider = { - getList: (resource, params) => Promise, // get a list of records based on sort, filter, and pagination - getOne: (resource, params) => Promise, // get a single record by id - getMany: (resource, params) => Promise, // get a list of records based on an array of ids - getManyReference: (resource, params) => Promise, // get the records referenced to another record, e.g. comments for a post - create: (resource, params) => Promise, // create a record - update: (resource, params) => Promise, // update a record based on a patch - updateMany: (resource, params) => Promise, // update a list of records based on an array of ids and a common patch - delete: (resource, params) => Promise, // delete a record by id - deleteMany: (resource, params) => Promise, // delete a list of records based on an array of ids -} -``` - -**Tip**: A Data Provider can have more methods than the 9 methods listed above. For instance, you create a dataProvider with custom methods for calling non-REST API endpoints, manipulating tree structures, subscribing to real time updates, etc. - -The Data Provider is at the heart of react-admin's architecture. By being very opinionated about the Data Provider interface, react-admin can be very flexible AND provide very sophisticated features, including reference handling, optimistic updates, and automated navigation. diff --git a/docs/DataProviderWriting.md b/docs/DataProviderWriting.md index 29591bdbede..e05a85ea3ab 100644 --- a/docs/DataProviderWriting.md +++ b/docs/DataProviderWriting.md @@ -9,106 +9,67 @@ APIs are so diverse that quite often, none of [the available Data Providers](./D The methods of a Data Provider receive a request, and return a promise for a response. Both the request and the response format are standardized. -**Caution**: A Data Provider should return the same shape in `getList` and `getOne` for a given resource. This is because react-admin uses "optimistic rendering", and renders the Edit and Show view *before* calling `dataProvider.getOne()` by reusing the response from `dataProvider.getList()` if the user has displayed the List view before. If your API has different shapes for a query for a unique record and for a query for a list of records, your Data Provider should make these records consistent in shape before returning them to react-admin. +## Data Provider Methods -For instance, the following Data Provider returns more details in `getOne` than in `getList`: +A data provider must implement the following methods: ```jsx -const { data } = await dataProvider.getList('posts', { - pagination: { page: 1, perPage: 5 }, - sort: { field: 'title', order: 'ASC' }, - filter: { author_id: 12 }, -}) -// [ -// { id: 123, title: "hello, world", author_id: 12 }, -// { id: 125, title: "howdy partner", author_id: 12 }, -// ], - -const { data } = dataProvider.getOne('posts', { id: 123 }) -// { -// data: { id: 123, title: "hello, world", author_id: 12, body: 'Lorem Ipsum Sic Dolor Amet' } -// } +const dataProvider = { + // get a list of records based on sort, filter, and pagination + getList: (resource, params) => Promise, + // get a single record by id + getOne: (resource, params) => Promise, + // get a list of records based on an array of ids + getMany: (resource, params) => Promise, + // get the records referenced to another record, e.g. comments for a post + getManyReference: (resource, params) => Promise, + // create a record + create: (resource, params) => Promise, + // update a record based on a patch + update: (resource, params) => Promise, + // update a list of records based on an array of ids and a common patch + updateMany: (resource, params) => Promise, + // delete a record by id + delete: (resource, params) => Promise, + // delete a list of records based on an array of ids + deleteMany: (resource, params) => Promise, +} ``` -This will cause the Edit view to blink on load. If you have this problem, modify your Data Provider to return the same shape for all methods. - -## Request Format - -Data queries require a *method* (e.g. `getOne`), a *resource* (e.g. 'posts') and a set of *parameters*. +To call the data provider, react-admin combines a *method* (e.g. `getOne`), a *resource* (e.g. 'posts') and a set of *parameters*. **Tip**: In comparison, HTTP requests require a *verb* (e.g. 'GET'), an *url* (e.g. 'http://myapi.com/posts'), a list of *headers* (like `Content-Type`) and a *body*. -Standard methods are: - -| Method | Usage | Parameters format | -| ------------------ | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | -| `getList` | Search for resources | `{ pagination: { page: {int} , perPage: {int} }, sort: { field: {string}, order: {string} }, filter: {Object}, meta: {Object} }` | -| `getOne` | Read a single resource, by id | `{ id: {mixed}, meta: {Object} }` | -| `getMany` | Read a list of resource, by ids | `{ ids: {mixed[]}, meta: {Object} }` | -| `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}, meta: {Object} }` | -| `create` | Create a single resource | `{ data: {Object}, meta: {Object} }` | -| `update` | Update a single resource | `{ id: {mixed}, data: {Object}, previousData: {Object}, meta: {Object} }` | -| `updateMany` | Update multiple resources | `{ ids: {mixed[]}, data: {Object}, meta: {Object} }` | -| `delete` | Delete a single resource | `{ id: {mixed}, previousData: {Object}, meta: {Object} }` | -| `deleteMany` | Delete multiple resources | `{ ids: {mixed[]}, meta: {Object} }` | +In the rest of this documentation, the term `Record` designates an object literal with at least an `id` property (e.g. `{ id: 123, title: "hello, world" }`). -**Tip**: All methods accept an optional `meta` parameter. React-admin doesn't use it, but it's a good way to pass special arguments or metadata to an API call. +## `getList` -Here are several examples of how react-admin can call the Data Provider: +React-admin calls `dataProvider.getList()` to search records. -```js -dataProvider.getList('posts', { - pagination: { page: 1, perPage: 5 }, - sort: { field: 'title', order: 'ASC' }, - filter: { author_id: 12 }, -}); -dataProvider.getOne('posts', { id: 123 }); -dataProvider.getMany('posts', { ids: [123, 124, 125] }); -dataProvider.getManyReference('comments', { - target: 'post_id', - id: 123, - sort: { field: 'created_at', order: 'DESC' } -}); -dataProvider.create('posts', { data: { title: "hello, world" } }); -dataProvider.update('posts', { - id: 123, - data: { title: "hello, world!" }, - previousData: { title: "previous title" } -}); -dataProvider.updateMany('posts', { - ids: [123, 234], - data: { views: 0 }, -}); -dataProvider.delete('posts', { - id: 123, - previousData: { title: "hello, world" } -}); -dataProvider.deleteMany('posts', { ids: [123, 234] }); +**Interface** +```tsx +interface GetListParams { + pagination: { page: number, perPage: number }; + sort: { field: string, order: 'ASC' | 'DESC' }; + filter: any; + meta?: any; +} +interface GetListResult { + data: Record[]; + total?: number; + // if using partial pagination + pageInfo?: { + hasNextPage?: boolean; + hasPreviousPage?: boolean; + }; +} +function getList(resource: string, params: GetListParams): Promise ``` -**Tip**: If your API supports more request types, you can add more methods to the Data Provider (for instance to support upserts, aggregations, or Remote Procedure Call). React-admin won't call these methods directly, but you can call them in your own component thanks to the `useDataProvider` hook described in the [Querying the API](./Actions.md) documentation. +**Example** -## Response Format - -Data Providers methods must return a Promise for an object with a `data` property. - -| Method | Response format | -| ------------------ | --------------------------------------------------------------- | -| `getList` | `{ data: {Record[]}, total: {int} }` | -| `getOne` | `{ data: {Record} }` | -| `getMany` | `{ data: {Record[]} }` | -| `getManyReference` | `{ data: {Record[]}, total: {int} }` | -| `create` | `{ data: {Record} }` | -| `update` | `{ data: {Record} }` | -| `updateMany` | `{ data: {mixed[]} }` The ids which have been updated | -| `delete` | `{ data: {Record} }` The record that has been deleted | -| `deleteMany` | `{ data: {mixed[]} }` The ids of the deleted records (optional) | - -A `{Record}` is an object literal with at least an `id` property, e.g. `{ id: 123, title: "hello, world" }`. - -Building up on the previous example, here are example responses matching the format expected by react-admin: - -```js +```jsx +// find the first 5 posts whose author_id is 12, sorted by title dataProvider.getList('posts', { pagination: { page: 1, perPage: 5 }, sort: { field: 'title', order: 'ASC' }, @@ -125,23 +86,99 @@ dataProvider.getList('posts', { // ], // total: 27 // } +``` + +## `getOne` + +React-admin calls `dataProvider.getOne()` to fetch a single record by `id`. + +**Interface** +```tsx +interface GetOneParams { + id: Identifier; + meta?: any; +} +interface GetOneResult { + data: Record; +} +function getOne(resource: string, params: GetOneParams): Promise +``` + +**Example** + +```jsx +// find post 123 dataProvider.getOne('posts', { id: 123 }) .then(response => console.log(response)); // { // data: { id: 123, title: "hello, world" } // } +``` + +## `getMany` + +React-admin calls `dataProvider.getMany()` to fetch several records at once using their `id`. + +**Interface** + +```tsx +interface GetManyParams { + ids: Identifier[]; + meta?: any; +} +interface GetManyResult { + data: Record[]; +} +function getMany(resource: string, params: GetManyParams): Promise +``` +**Example** + +```jsx +// find posts 123, 124 and 125 dataProvider.getMany('posts', { ids: [123, 124, 125] }) .then(response => console.log(response)); // { // data: [ // { id: 123, title: "hello, world" }, -// { id: 124, title: "good day sunshise" }, +// { id: 124, title: "good day sunshine" }, // { id: 125, title: "howdy partner" }, // ] // } +``` + +## `getManyReference` + +React-admin calls `dataProvider.getManyReference()` to fetch several records related to another one. + +**Interface** + +```tsx +interface GetManyReferenceParams { + target: string; + id: Identifier; + pagination: { page: number, perPage: number }; + sort: { field: string, order: 'ASC' | 'DESC' }; + filter: any; + meta?: any; +} +interface GetManyReferenceResult { + data: Record[]; + total?: number; + // if using partial pagination + pageInfo?: { + hasNextPage?: boolean; + hasPreviousPage?: boolean; + }; +} +function getManyReference(resource: string, params: GetManyReferenceParams): Promise +``` + +**Example** +```jsx +// find all comments related to post 123 dataProvider.getManyReference('comments', { target: 'post_id', id: 123, @@ -156,23 +193,93 @@ dataProvider.getManyReference('comments', { // ], // total: 2, // } +``` + +## `create` + +React-admin calls `dataProvider.create()` to create a new record. + +**Interface** + +```tsx +interface CreateParams { + data: Partial; + meta?: any; +} + +interface CreateResult { + data: Record; +} +function create(resource: string, params: CreateParams): Promise +``` + +**Example** +```jsx +// create a new post with title "hello, world" dataProvider.create('posts', { data: { title: "hello, world" } }) .then(response => console.log(response)); // { // data: { id: 450, title: "hello, world" } // } +``` + +## `update` + +React-admin calls `dataProvider.update()` to update a record. + +**Interface** + +```tsx +interface UpdateParams { + id: Identifier; + data: Partial; + previousData: Record; + meta?: any; +} +interface UpdateResult { + data: Record; +} +function update(resource: string, params: UpdateParams): Promise +``` + +**Example** +```jsx +// update post 123 with title "hello, world!" dataProvider.update('posts', { id: 123, data: { title: "hello, world!" }, - previousData: { title: "previous title" } + previousData: { id: 123, title: "previous title" } }) .then(response => console.log(response)); // { // data: { id: 123, title: "hello, world!" } // } +``` + +## `updateMany` + +React-admin calls `dataProvider.updateMany()` to update several records by `id` with a unified changeset. + +**Interface** + +```tsx +interface UpdateManyParams { + ids: Identifier[]; + data: Partial; + meta?: any; +} +interface UpdateManyResult { + data: Identifier[]; +} +function updateMany(resource: string, params: UpdateManyParams): Promise +``` +**Example** + +```jsx +// update posts 123 and 234 to set views to 0 dataProvider.updateMany('posts', { ids: [123, 234], data: { views: 0 }, @@ -181,16 +288,61 @@ dataProvider.updateMany('posts', { // { // data: [123, 234] // } +``` + +## `delete` + +React-admin calls `dataProvider.delete()` to delete a record by `id`. + +**Interface** + +```tsx +interface DeleteParams { + id: Identifier; + previousData?: Record; + meta?: any; +} +interface DeleteResult { + data: Record; +} +function delete(resource: string, params: DeleteParams): Promise +``` + +**Example** +```jsx +// delete post 123 dataProvider.delete('posts', { id: 123, - previousData: { title: "hello, world!" } + previousData: { id: 123, title: "hello, world!" } }) .then(response => console.log(response)); // { // data: { id: 123, title: "hello, world" } // } +``` + +## `deleteMany` +React-admin calls `dataProvider.deleteMany()` to delete several records by `id`. + +**Interface** + +```tsx +interface DeleteManyParams { + ids: Identifier[]; + meta?: any; +} +interface DeleteManyResult { + data: Identifier[]; +} +function deleteMany(resource: string, params: DeleteManyParams): Promise +``` + +**Example** + +```jsx +// delete posts 123 and 234 dataProvider.deleteMany('posts', { ids: [123, 234] }) .then(response => console.log(response)); // { @@ -273,6 +425,46 @@ export default { }; ``` +## The `meta` Parameter + +All data provider methods accept a `meta` parameter. React-admin core components never set this `meta` when calling the data provider. It's designed to let you pass additional parameters to your data provider. + +For instance, you could pass an option to embed related records in the response: + +```jsx +const { data, isLoading, error } = useGetOne( + 'books', + { id, meta: { _embed: 'authors' } }, +); +``` + +It's up to you to use this `meta` parameter in your data provider. + +## `getList` and `getOne` Shared Cache + +A Data Provider should return the same shape in `getList` and `getOne` for a given resource. This is because react-admin uses "optimistic rendering", and renders the Edit and Show view *before* calling `dataProvider.getOne()` by reusing the response from `dataProvider.getList()` if the user has displayed the List view before. If your API has different shapes for a query for a unique record and for a query for a list of records, your Data Provider should make these records consistent in shape before returning them to react-admin. + +For instance, the following Data Provider returns more details in `getOne` than in `getList`: + +```jsx +const { data } = await dataProvider.getList('posts', { + pagination: { page: 1, perPage: 5 }, + sort: { field: 'title', order: 'ASC' }, + filter: { author_id: 12 }, +}) +// [ +// { id: 123, title: "hello, world", author_id: 12 }, +// { id: 125, title: "howdy partner", author_id: 12 }, +// ], + +const { data } = dataProvider.getOne('posts', { id: 123 }) +// { +// data: { id: 123, title: "hello, world", author_id: 12, body: 'Lorem Ipsum Sic Dolor Amet' } +// } +``` + +This will cause the Edit view to blink on load. If you have this problem, modify your Data Provider to return the same shape for all methods. + ## Example REST Implementation Let's say that you want to map the react-admin requests to a REST backend exposing the following API: @@ -395,7 +587,7 @@ const apiUrl = 'https://my.api.com/'; const httpClient = fetchUtils.fetchJson; export default { - getList: (resource, params) => { + getList: async (resource, params) => { const { page, perPage } = params.pagination; const { field, order } = params.sort; const query = { @@ -404,27 +596,29 @@ export default { filter: JSON.stringify(params.filter), }; const url = `${apiUrl}/${resource}?${stringify(query)}`; - - return httpClient(url).then(({ headers, json }) => ({ + const { json, headers } = await httpClient(url); + return { data: json, total: parseInt(headers.get('content-range').split('/').pop(), 10), - })); + }; }, - getOne: (resource, params) => - httpClient(`${apiUrl}/${resource}/${params.id}`).then(({ json }) => ({ - data: json, - })), + getOne: async (resource, params) => { + const url = `${apiUrl}/${resource}/${params.id}` + const { json } = await httpClient(url); + return { data: json }; + }, - getMany: (resource, params) => { + getMany: async (resource, params) => { const query = { filter: JSON.stringify({ ids: params.ids }), }; const url = `${apiUrl}/${resource}?${stringify(query)}`; - return httpClient(url).then(({ json }) => ({ data: json })); + const { json } = await httpClient(url); + return { data: json }; }, - getManyReference: (resource, params) => { + getManyReference: async (resource, params) => { const { page, perPage } = params.pagination; const { field, order } = params.sort; const query = { @@ -436,50 +630,60 @@ export default { }), }; const url = `${apiUrl}/${resource}?${stringify(query)}`; - - return httpClient(url).then(({ headers, json }) => ({ + const { json, headers } = await httpClient(url); + return { data: json, total: parseInt(headers.get('content-range').split('/').pop(), 10), - })); + }; }, - create: (resource, params) => - httpClient(`${apiUrl}/${resource}`, { + create: async (resource, params) => { + const { json } = await httpClient(`${apiUrl}/${resource}`, { method: 'POST', body: JSON.stringify(params.data), - }).then(({ json }) => ({ - data: { ...params.data, id: json.id }, - })), + }) + return { data: { ...params.data, id: json.id } }; + }, - update: (resource, params) => - httpClient(`${apiUrl}/${resource}/${params.id}`, { + update: async (resource, params) => { + const url = `${apiUrl}/${resource}/${params.id}`; + const { json } = await httpClient(url, { method: 'PUT', body: JSON.stringify(params.data), - }).then(({ json }) => ({ data: json })), + }) + return { data: json }; + }, - updateMany: (resource, params) => { + updateMany: async (resource, params) => { const query = { filter: JSON.stringify({ id: params.ids}), }; - return httpClient(`${apiUrl}/${resource}?${stringify(query)}`, { + const url = `${apiUrl}/${resource}?${stringify(query)}`; + const { json } = await httpClient(url, { method: 'PUT', body: JSON.stringify(params.data), - }).then(({ json }) => ({ data: json })); + }) + return { data: json }; }, - delete: (resource, params) => - httpClient(`${apiUrl}/${resource}/${params.id}`, { + delete: async (resource, params) => { + const url = `${apiUrl}/${resource}/${params.id}`; + const { json } = await httpClient(url, { method: 'DELETE', - }).then(({ json }) => ({ data: json })), + }); + return { data: json }; + }, - deleteMany: (resource, params) => { + deleteMany: async (resource, params) => { const query = { filter: JSON.stringify({ id: params.ids}), }; - return httpClient(`${apiUrl}/${resource}?${stringify(query)}`, { + const url = `${apiUrl}/${resource}?${stringify(query)}`; + const { json } = await httpClient(url, { method: 'DELETE', body: JSON.stringify(params.data), - }).then(({ json }) => ({ data: json })); + }); + return { data: json }; }, }; ``` diff --git a/docs/DataProviders.md b/docs/DataProviders.md index f9bd7bff5f7..9667cb1fef6 100644 --- a/docs/DataProviders.md +++ b/docs/DataProviders.md @@ -1,14 +1,60 @@ --- layout: default -title: "Using Data Providers" +title: "Data Fetching" --- -# Setting Up The Data Provider +# Data Fetching + +You can build a react-admin app on top of any API, whether it uses REST, GraphQL, RPC, or even SOAP, regardless of the dialect it uses. This works because react-admin doesn't use `fetch` directly. Instead, it uses a Data Provider object to interface with your API, and [react-query](https://tanstack.com/query/v3/docs/react/overview) to handle data fetching. + +## The Data Provider + +Whenever react-admin needs to communicate with your APIs, it does it through an object called the `dataProvider`. The `dataProvider` exposes a predefined interface that allows react-admin to query any API in a normalized way. + +Backend agnostic + +For instance, to query the API for a single record, react-admin calls `dataProvider.getOne()`: + +```js +dataProvider + .getOne('posts', { id: 123 }) + .then(response => { + console.log(response.data); // { id: 123, title: "hello, world" } + }); +``` + +It's the Data Provider's job to turn these method calls into HTTP requests, and transform the HTTP responses to the data format expected by react-admin. In technical terms, a Data Provider is an *adapter* for an API. + +Thanks to this adapter system, react-admin can communicate with any API. Check out the [list of supported backends](./DataProviderList.md) to pick an open-source package for your API. + +You can also [Write your own Data Provider](./DataProviderWriting.md) so that it fits the particularities of your backend(s). Data Providers can use `fetch`, `axios`, `apollo-client`, or any other library to communicate with APIs. The Data Provider is also the ideal place to add custom HTTP headers, authentication, etc. + +A Data Provider must have the following methods: + +```jsx +const dataProvider = { + getList: (resource, params) => Promise, // get a list of records based on sort, filter, and pagination + getOne: (resource, params) => Promise, // get a single record by id + getMany: (resource, params) => Promise, // get a list of records based on an array of ids + getManyReference: (resource, params) => Promise, // get the records referenced to another record, e.g. comments for a post + create: (resource, params) => Promise, // create a record + update: (resource, params) => Promise, // update a record based on a patch + updateMany: (resource, params) => Promise, // update a list of records based on an array of ids and a common patch + delete: (resource, params) => Promise, // delete a record by id + deleteMany: (resource, params) => Promise, // delete a list of records based on an array of ids +} +``` + +**Tip**: A Data Provider can have [more methods](#adding-custom-methods) than the 9 methods listed above. For instance, you create a dataProvider with custom methods for calling non-REST API endpoints, manipulating tree structures, subscribing to real time updates, etc. + +The Data Provider is at the heart of react-admin's architecture. By being very opinionated about the Data Provider interface, react-admin can be very flexible AND provide very sophisticated features, including reference handling, optimistic updates, and automated navigation. ## `` The first step to use a Data Provider is to pass it to [the `` component](./Admin.md). You can do so by using the `dataProvider` prop. +You can either pick a data provider from the list of [supported API backends](./DataProviderList.md), or [write your own](./DataProviderWriting.md). + As an example, let's focus on [the Simple REST data provider](https://github.com/marmelab/react-admin/tree/master/packages/ra-data-simple-rest). It fits REST APIs using simple GET parameters for filters and sorting. Install the `ra-data-simple-rest` package to use this provider. @@ -40,7 +86,7 @@ export default App; That's enough to make all react-admin components work. -Here is how this Data Provider maps react-admin calls to API calls: +Here is how this particular Data Provider maps react-admin calls to API calls: | Method name | API call | | ------------------ | --------------------------------------------------------------------------------------- | @@ -287,9 +333,9 @@ Check the [withLifecycleCallbacks](./withLifecycleCallbacks.md) documentation fo ## Handling File Uploads -You can leverage [`withLifecycleCallbacks`](#adding-lifecycle-callbacks) to add support for file upload. +Handling file uploads in react-admin depends on how your server expects the file to be sent (e.g. as a Base64 string, as a multipart/form-data request, uploaded to a CDN via an AJAX request, etc.). When a user submits a form with a file input, the dataProvider method (`create` or `delete`) receives [a `File` object](https://developer.mozilla.org/en-US/docs/Web/API/File). It's the dataProvider's job to convert that `File`, e.g. using the `FileReader` API. -For instance, the following Data Provider extends the `ra-data-simple-rest` provider, and stores images passed to the `dataProvider.update('posts')` call as Base64 strings. React-admin offers an `` component that allows image upload: +For instance, the following Data Provider extends an existing data provider to convert images passed to `dataProvider.update('posts')` into Base64 strings. The example leverages [`withLifecycleCallbacks`](#adding-lifecycle-callbacks) to modify the `dataProvider.update()` method for the `posts` resource only. ```js import { withLifecycleCallbacks } from 'react-admin'; @@ -298,7 +344,7 @@ import simpleRestProvider from 'ra-data-simple-rest'; const dataProvider = withLifecycleCallbacks(simpleRestProvider('http://path.to.my.api/'), [ { /** - * For posts update only, convert uploaded image in base 64 and attach it to + * For posts update only, convert uploaded images to base 64 and attach them to * the `picture` sent property, with `src` and `title` attributes. */ resource: 'posts', @@ -311,24 +357,20 @@ const dataProvider = withLifecycleCallbacks(simpleRestProvider('http://path.to.m p => !(p.rawFile instanceof File) ); - return Promise.all(newPictures.map(convertFileToBase64)) - .then(base64Pictures => - base64Pictures.map(picture64 => ({ - src: picture64, - title: `${params.data.title}`, - })) - ) - .then(transformedNewPictures => - dataProvider.update(resource, { - data: { - ...params.data, - pictures: [ - ...transformedNewPictures, - ...formerPictures, - ], - }, - }) - ); + const base64Pictures = await Promise.all( + newPictures.map(convertFileToBase64) + ) + const pictures = [ + ...base64Pictures.map((dataUrl, index) => ({ + src: dataUrl, + title: newPictures[index].name, + })), + ...formerPictures, + ]; + return dataProvider.update( + resource, + { data: { ...params.data, pictures } } + ); } } ]); @@ -343,14 +385,13 @@ const convertFileToBase64 = file => const reader = new FileReader(); reader.onload = () => resolve(reader.result); reader.onerror = reject; - reader.readAsDataURL(file.rawFile); }); export default myDataProvider; ``` -**Tip**: use `beforeSave` instead of `beforeUpdate` to do the same for both create and update calls. +**Tip**: Use `beforeSave` instead of `beforeUpdate` to do the same for both create and update calls. You can use the same technique to upload images to an object storage service, and then update the record using the URL of that stored object. @@ -399,7 +440,7 @@ Check the [Calling Custom Methods](./Actions.md#calling-custom-methods) document ## Async Initialization -Some Data Providers need an asynchronous initialization phase (e.g. to connect to the API). To use such Data Providers, initialize them before rendering react-admin resources, leveraging React's `useState` and `useEffect`. +Some Data Providers need an asynchronous initialization phase (e.g. to connect to the API). To use such Data Providers, initialize them *before* rendering react-admin resources, leveraging React's `useState` and `useEffect`. For instance, the `ra-data-hasura` data provider needs to be initialized: @@ -416,8 +457,9 @@ const App = () => { // initialize on mount useEffect(() => { - buildHasuraProvider({ clientOptions: { uri: 'http://localhost:8080/v1/graphql' } }) - .then(() => setDataProvider(() => dataProvider)); + buildHasuraProvider({ + clientOptions: { uri: 'http://localhost:8080/v1/graphql' } + }).then(() => setDataProvider(() => dataProvider)); }, []); // hide the admin until the data provider is ready @@ -435,32 +477,6 @@ export default App; **Tip**: This example uses the function version of `setState` (`setDataProvider(() => dataProvider))`) instead of the more classic version (`setDataProvider(dataProvider)`). This is because some legacy Data Providers are actually functions, and `setState` would call them immediately on mount. -## Default Query Options - -If you often need to pass the same query options to the data provider, you can use [the `` prop](./Admin.md#queryclient) to set them globally. - -```jsx -import { Admin } from 'react-admin'; -import { QueryClient } from 'react-query'; - -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - retry: false, - staleTime: Infinity, - }, - } -}); - -const App = () => ( - - ... - -); -``` - -To know which query options you can override, check the [Querying the API documentation](./Actions.md#query-options) and [the `` prop documentation](./Admin.md#queryclient). - ## Combining Data Providers If you need to build an app relying on more than one API, you may face a problem: the `` component accepts only one `dataProvider` prop. You can combine multiple data providers into one using the `combineDataProviders` helper. It expects a function as parameter accepting a resource name and returning a data provider for that resource. @@ -532,3 +548,106 @@ export const App = () => ( ); ``` + +## React-Query Options + +React-admin uses [react-query](https://react-query-v3.tanstack.com/) to fetch, cache and update data. Internally, the `` component creates a react-query [`QueryClient`](https://tanstack.com/query/v3/docs/react/reference/QueryClient) on mount, using [react-query's "aggressive but sane" defaults](https://react-query-v3.tanstack.com/guides/important-defaults): + +* Queries consider cached data as stale +* Stale queries are refetched automatically in the background when: + * New instances of the query mount + * The window is refocused + * The network is reconnected + * The query is optionally configured with a refetch interval +* Query results that are no longer used in the current page are labeled as "inactive" and remain in the cache in case they are used again at a later time. +* By default, "inactive" queries are garbage collected after 5 minutes. +* Queries that fail are silently retried 3 times, with exponential backoff delay before capturing and displaying an error to the UI. +* Query results by default are structurally shared to detect if data has actually changed and if not, the data reference remains unchanged to better help with value stabilization with regards to `useMemo` and `useCallback`. + +If you want to override the react-query default query and mutation default options, or use a specific client or mutation cache, you can create your own `QueryClient` instance and pass it to the `` prop: + +```jsx +import { Admin } from 'react-admin'; +import { QueryClient } from 'react-query'; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + structuralSharing: false, + }, + mutations: { + retryDelay: 10000, + }, + }, +}); + +const App = () => ( + + ... + +); +``` + +To know which options you can pass to the `QueryClient` constructor, check the [react-query documentation](https://tanstack.com/query/v3/docs/react/reference/QueryClient) and the [query options](https://tanstack.com/query/v3/docs/react/reference/useQuery) and [mutation options](https://tanstack.com/query/v3/docs/react/reference/useMutation) sections. + +The settings that react-admin developers often overwrite are: + +```jsx +import { QueryClient } from 'react-query'; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + /** + * The time in milliseconds after data is considered stale. + * If set to `Infinity`, the data will never be considered stale. + */ + staleTime: 10000, + /** + * If `false`, failed queries will not retry by default. + * If `true`, failed queries will retry infinitely., failureCount: num + * If set to an integer number, e.g. 3, failed queries will retry until the failed query count meets that number. + * If set to a function `(failureCount, error) => boolean` failed queries will retry until the function returns false. + */ + retry: false, + /** + * If set to `true`, the query will refetch on window focus if the data is stale. + * If set to `false`, the query will not refetch on window focus. + * If set to `'always'`, the query will always refetch on window focus. + * If set to a function, the function will be executed with the latest data and query to compute the value. + * Defaults to `true`. + */ + refetchOnWindowFocus: false, + }, + }, +}); +``` + +## Calling The Data Provider + +You can call the data provider directly from your own React components, combining it with react-query's `useQuery` and `useMutation` hooks. However, this is such a common use case that react-admin provides a hook for each of the data provider methods. + +For instance, to call `dataProvider.getOne()`, use the `useGetOne` hook: + +```jsx +import { useGetOne } from 'react-admin'; +import { Loading, Error } from './MyComponents'; + +const UserProfile = ({ userId }) => { + const { data: user, isLoading, error } = useGetOne('users', { id: userId }); + + if (isLoading) return ; + if (error) return ; + if (!user) return null; + + return ( +
    +
  • Name: {user.name}
  • +
  • Email: {user.email}
  • +
+ ) +}; +``` + +The [Querying the API](./Actions.md) documentation lists all the hooks available for querying the API, as well as the options and return values for each of them. \ No newline at end of file diff --git a/docs/EditTutorial.md b/docs/EditTutorial.md index 1d33ee6f8a5..c89728a0d0b 100644 --- a/docs/EditTutorial.md +++ b/docs/EditTutorial.md @@ -19,7 +19,7 @@ To better understand how to use the various react-admin hooks and components ded ### An Edition View Built By Hand -Here is how you could write a book edition view in pure React, leveraging react-admin's [data fetching hooks](./DataProviderIntroduction.md), and [react-hook-form](https://react-hook-form.com/) to bind form inputs with a record object: +Here is how you could write a book edition view in pure React, leveraging react-admin's [data fetching hooks](./Actions.md), and [react-hook-form](https://react-hook-form.com/) to bind form inputs with a record object: ```jsx import * as React from "react"; diff --git a/docs/Features.md b/docs/Features.md index dd85f757cb5..deac2e86eb1 100644 --- a/docs/Features.md +++ b/docs/Features.md @@ -59,7 +59,7 @@ Which kind of API? **All kinds**. React-admin is backend agnostic. It doesn't ca Backend agnostic -React-admin ships with [more than 50 adapters](./DataProviderList.md) for popular API flavors, and gives you all the tools to build your own adapter. This works thanks to a powerful abstraction layer called the [Data Provider](./DataProviderIntroduction.md). +React-admin ships with [more than 50 adapters](./DataProviderList.md) for popular API flavors, and gives you all the tools to build your own adapter. This works thanks to a powerful abstraction layer called the [Data Provider](./DataProviders.md). In a react-admin app, you don't write API Calls. Instead, you communicate with your API using a set of high-level functions, called "Data Provider methods". For instance, to fetch a list of posts, you call the `getList()` method, passing the resource name and the query parameters. diff --git a/docs/Resource.md b/docs/Resource.md index b72456d3f5c..6891e7c284e 100644 --- a/docs/Resource.md +++ b/docs/Resource.md @@ -49,7 +49,7 @@ The routes call the following `dataProvider` methods: * `edit` calls `getOne()` on mount, and `update()` or `delete()` on submission * `create` calls `create()` on submission -**Tip**: Which API endpoint does a resource rely on? The `` component doesn't know this mapping - it's [the `dataProvider`'s job](./DataProviderIntroduction.md) to define it. +**Tip**: Which API endpoint does a resource rely on? The `` component doesn't know this mapping - it's [the `dataProvider`'s job](./DataProviders.md) to define it. ## `name` diff --git a/docs/ShowTutorial.md b/docs/ShowTutorial.md index ec19c90e501..5182c7cb0b2 100644 --- a/docs/ShowTutorial.md +++ b/docs/ShowTutorial.md @@ -17,7 +17,7 @@ To better understand how to use the various react-admin hooks and components ded ### A Show View Built By Hand -Here is how you could write a simple book show view, leveraging react-admin's [data fetching hooks](./DataProviderIntroduction.md): +Here is how you could write a simple book show view, leveraging react-admin's [data fetching hooks](./DataProviders.md): ```jsx import { useParams } from 'react-router-dom'; diff --git a/docs/Tutorial.md b/docs/Tutorial.md index aebb2e198c2..c2e0d71af42 100644 --- a/docs/Tutorial.md +++ b/docs/Tutorial.md @@ -1156,7 +1156,7 @@ Now that you've completed the tutorial, continue your journey with [the Features After that, the best way to learn react-admin is by reading the introduction chapters to each of its major parts: -- [Data Provider and API Calls](./DataProviderIntroduction.md) +- [Data Provider and API Calls](./DataProviders.md) - [Auth Provider and Security](./Authentication.md) - [List Page](./ListTutorial.md) - [Creation & Edition Pages](./EditTutorial.md) diff --git a/docs/addRefreshAuthToDataProvider.md b/docs/addRefreshAuthToDataProvider.md index d4050bb10c1..604c1cec5c9 100644 --- a/docs/addRefreshAuthToDataProvider.md +++ b/docs/addRefreshAuthToDataProvider.md @@ -5,7 +5,7 @@ title: "addRefreshAuthToDataProvider" # `addRefreshAuthToDataProvider` -This helper function wraps an existing [`dataProvider`](./DataProviderIntroduction.md) to support authentication token refreshing mechanisms. +This helper function wraps an existing [`dataProvider`](./DataProviders.md) to support authentication token refreshing mechanisms. ## Usage diff --git a/docs/fetchJson.md b/docs/fetchJson.md index 8ab0ea25bbb..6797fc69384 100644 --- a/docs/fetchJson.md +++ b/docs/fetchJson.md @@ -15,7 +15,7 @@ React-admin includes a `fetchJson` utility function to make HTTP calls. It's a w ## Usage -You can use it to make HTTP calls directly, to build a custom [`dataProvider`](./DataProviderIntroduction.md), or pass it directly to any `dataProvider` that supports it, such as [`ra-data-simple-rest`](https://github.com/marmelab/react-admin/tree/master/packages/ra-data-simple-rest). +You can use it to make HTTP calls directly, to build a custom [`dataProvider`](./DataProviders.md), or pass it directly to any `dataProvider` that supports it, such as [`ra-data-simple-rest`](https://github.com/marmelab/react-admin/tree/master/packages/ra-data-simple-rest). ```jsx import { fetchUtils, Admin, Resource } from 'react-admin'; diff --git a/docs/navigation.html b/docs/navigation.html index 060a6fcf9c2..a344aeff025 100644 --- a/docs/navigation.html +++ b/docs/navigation.html @@ -19,9 +19,8 @@
    Data Fetching
    -
  • Introduction
  • +
  • Introduction
  • Supported backends
  • -
  • Setting Up
  • Writing A Data Provider
  • Querying the API
  • Real-time Updates & Locks
  • diff --git a/docs/useCreate.md b/docs/useCreate.md index 459403761fa..d4fe2debc73 100644 --- a/docs/useCreate.md +++ b/docs/useCreate.md @@ -7,8 +7,9 @@ title: "useCreate" This hook allows to call `dataProvider.create()` when the callback is executed. +## Syntax + ```jsx -// syntax const [create, { data, isLoading, error }] = useCreate( resource, { data, meta }, @@ -28,6 +29,8 @@ create( 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 `create` callback (second example below). +## Usage + ```jsx // set params when calling the hook import { useCreate, useRecordContext } from 'react-admin'; diff --git a/docs/useDataProvider.md b/docs/useDataProvider.md index 823a439eec1..ce0f6ecb716 100644 --- a/docs/useDataProvider.md +++ b/docs/useDataProvider.md @@ -7,7 +7,18 @@ title: "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: +## Syntax + +The hook takes no parameter and returns the Data Provider: +```jsx +const dataProvider = useDataProvider(); +``` + +**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()`). + +## Usage + +Here is how to query the Data Provider for the current user profile: ```jsx import { useState, useEffect } from 'react'; @@ -46,8 +57,6 @@ const UserProfile = ({ userId }) => { But the recommended way to query the Data Provider is to use the dataProvider method hooks (like [`useGetOne`](./useGetOne.md) for instance). -**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()`). - ## TypeScript The `useDataProvider` hook accepts a generic parameter for the `dataProvider` type. This is useful when you added custom methods to your `dataProvider`: diff --git a/docs/useDelete.md b/docs/useDelete.md index ed3849eb6dd..6c0a649570c 100644 --- a/docs/useDelete.md +++ b/docs/useDelete.md @@ -7,8 +7,9 @@ title: "useDelete" This hook allows calling `dataProvider.delete()` when the callback is executed and deleting a single record based on its `id`. +## Syntax + ```jsx -// syntax const [deleteOne, { data, isLoading, error }] = useDelete( resource, { id, previousData, meta }, @@ -28,6 +29,8 @@ deleteOne( 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 `deleteOne` callback (second example below). +## Usage + ```jsx // set params when calling the hook import { useDelete, useRecordContext } from 'react-admin'; diff --git a/docs/useDeleteMany.md b/docs/useDeleteMany.md index 93bffedbe5d..8fdda859046 100644 --- a/docs/useDeleteMany.md +++ b/docs/useDeleteMany.md @@ -7,8 +7,9 @@ title: "useDeleteMany" This hook allows to call `dataProvider.deleteMany()` when the callback is executed, and delete an array of records based on their `ids`. +## Syntax + ```jsx -// syntax const [deleteMany, { data, isLoading, error }] = useDeleteMany( resource, { ids, meta }, @@ -28,6 +29,8 @@ deleteMany( 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 `deleteMany` callback (second example below). +## Usage + ```jsx // set params when calling the hook import { useDeleteMany } from 'react-admin'; diff --git a/docs/useGetList.md b/docs/useGetList.md index 809b287cfbc..74d46aea239 100644 --- a/docs/useGetList.md +++ b/docs/useGetList.md @@ -13,11 +13,52 @@ This hook calls `dataProvider.getList()` when the component mounts. It's ideal f ```jsx const { data, total, isLoading, error, refetch } = useGetList( resource, - { pagination, sort, filter, meta }, + { + pagination: { page, perPage }, + sort: { field, order }, + filter, + meta + }, options ); ``` +The `meta` argument is optional. It can be anything you want to pass to the data provider, e.g. a list of fields to show in the result. + +The `options` parameter is optional, and is passed to [react-query's `useQuery` hook](https://tanstack.com/query/v3/docs/react/reference/useQuery). It may contain the following options: + +* `cacheTime` +* `enabled` +* `initialData` +* `initialDataUpdatedAt` +* `isDataEqual` +* `keepPreviousData` +* `meta` +* `notifyOnChangeProps` +* `notifyOnChangePropsExclusions` +* `onError` +* `onSettled` +* `onSuccess` +* `placeholderData` +* `queryKeyHashFn` +* `refetchInterval` +* `refetchIntervalInBackground` +* `refetchOnMount` +* `refetchOnReconnect` +* `refetchOnWindowFocus` +* `retry` +* `retryOnMount` +* `retryDelay` +* `select` +* `staleTime` +* `structuralSharing` +* `suspense` +* `useErrorBoundary` + +Check [react-query's `useQuery` hook documentation](https://tanstack.com/query/v3/docs/react/reference/useQuery) for details on each of these options. + +The react-query [query key](https://react-query-v3.tanstack.com/guides/query-keys) for this hook is `[resource, 'getList', { pagination, sort, filter, meta }]`. + ## Usage ```jsx diff --git a/docs/useGetMany.md b/docs/useGetMany.md index 3c0c7954973..4ed876613a6 100644 --- a/docs/useGetMany.md +++ b/docs/useGetMany.md @@ -7,15 +7,19 @@ title: "useGetMany" This hook calls `dataProvider.getMany()` when the component mounts. It queries the data provider for several records, based on an array of `ids`. +## Syntax + ```jsx -// syntax const { data, isLoading, error, refetch } = useGetMany( resource, { ids, meta }, options ); +``` -// example +## Usage + +```jsx import { useGetMany, useRecordContext } from 'react-admin'; const PostTags = () => { diff --git a/docs/useUpdate.md b/docs/useUpdate.md index e921a7e98f1..c2638889f4a 100644 --- a/docs/useUpdate.md +++ b/docs/useUpdate.md @@ -7,8 +7,9 @@ title: "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. +## Syntax + ```jsx -// syntax const [update, { data, isLoading, error }] = useUpdate( resource, { id, data, previousData }, @@ -28,6 +29,8 @@ update( 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 `update` callback (second example below). +## Usage + ```jsx // set params when calling the hook import { useUpdate, useRecordContext } from 'react-admin'; diff --git a/docs/useUpdateMany.md b/docs/useUpdateMany.md index f9123b31771..ff8e3158b0c 100644 --- a/docs/useUpdateMany.md +++ b/docs/useUpdateMany.md @@ -7,9 +7,9 @@ title: "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. +## Syntax ```jsx -// syntax const [updateMany, { data, isLoading, error }] = useUpdateMany( resource, { ids, data }, @@ -29,6 +29,8 @@ updateMany( 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 `updateMany` callback (second example below). +## Usage + ```jsx // set params when calling the hook import { useUpdateMany, useListContext } from 'react-admin'; diff --git a/docs/withLifecycleCallbacks.md b/docs/withLifecycleCallbacks.md index d65aa7dcab2..abc4e1af34e 100644 --- a/docs/withLifecycleCallbacks.md +++ b/docs/withLifecycleCallbacks.md @@ -5,7 +5,7 @@ title: "withLifecycleCallbacks" # `withLifecycleCallbacks` -This helper function adds logic to an existing [`dataProvider`](./DataProviderIntroduction.md) for particular resources, using pre- and post- event handlers like `beforeGetOne` and `afterSave`. +This helper function adds logic to an existing [`dataProvider`](./DataProviders.md) for particular resources, using pre- and post- event handlers like `beforeGetOne` and `afterSave`. **Note**: It's always preferable to **define custom business logic on the server side**. This helper is useful when you can't alter the underlying API, but has some serious [limitations](#limitations).