Querying the API
React-admin provides special hooks to emit read and write queries to the dataProvider
, which in turn sends requests to your API. Under the hood, it uses react-query to call the dataProvider
and cache the results.
Getting The dataProvider
Instance
React-admin stores the dataProvider
object in a React context, so it’s available from anywhere in your application code. The useDataProvider
hook grabs the Data Provider from that context, so you can call it directly.
For instance, here is how to query the Data Provider for the current user profile:
import { useDataProvider } from 'react-admin';
const MyComponent = () => {
const dataProvider = useDataProvider();
// ...
}
Refer to the useDataProvider
hook documentation for more information.
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()
).
DataProvider Method Hooks
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.
Their signature is the same as the related dataProvider method, e.g.:
// calls dataProvider.getOne(resource, { id })
const { data, isLoading, error } = useGetOne(resource, { id });
For instance, here is how to fetch one record from the API using the useGetOne
hook:
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 <Loading />;
if (error) return <Error />;
if (!user) return null;
return (
<ul>
<li>Name: {user.name}</li>
<li>Email: {user.email}</li>
</ul>
)
};
Tip: If you use TypeScript, you can specify the record type for more type safety:
const { data, isLoading } = useGetOne<Product>('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:
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:
For instance, here is an example using useUpdate()
:
import * as React from 'react';
import { useUpdate, useRecordContext, Button } from 'react-admin';
const ApproveButton = () => {
const record = useRecordContext();
const [approve, { isLoading }] = useUpdate('comments', { id: record.id, data: { isApproved: true }, previousData: record });
return <Button label="Approve" onClick={() => approve()} disabled={isLoading} />;
};
Both the query and mutation hooks accept an options
argument, to override the query options:
const { data: user, isLoading, error } = useGetOne(
'users',
{ id: userId },
{ enabled: userId !== undefined }
);
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.
const { data, isLoading, error } = useGetOne(
'books',
{ id, meta: { _embed: 'authors' } },
);
It’s up to the Data Provider to interpret this parameter.
useQuery
and useMutation
Internally, react-admin uses react-query 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:
- It triggers the loader in the AppBar when the query is running.
- It reduces the boilerplate code since you don’t need to use
useState
. - It supports a vast array of options
- 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
: fetches the dataProvider on mount. This is for read queries.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:
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(
['users', 'getOne', { id: userId }],
() => dataProvider.getOne('users', { id: userId })
);
if (isLoading) return <Loading />;
if (error) return <Error />;
if (!data) return null;
return (
<ul>
<li>Name: {data.data.name}</li>
<li>Email: {data.data.email}</li>
</ul>
)
};
To illustrate the usage of useMutation
, here is an implementation of an “Approve” button for a comment:
import * as React from 'react';
import { useMutation } from 'react-query';
import { useDataProvider, useRecordContext, Button } from 'react-admin';
const ApproveButton = () => {
const record = useRecordContext();
const dataProvider = useDataProvider();
const { mutate, isLoading } = useMutation(
() => dataProvider.update('comments', { id: record.id, data: { isApproved: true } })
);
return <Button label="Approve" onClick={() => mutate()} disabled={isLoading} />;
};
If you want to go beyond data provider method hooks, we recommend that you read the react-query documentation.
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. Here is how they defined these two variables:
isLoading
: The query has no data and is currently fetchingisFetching
: 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:
- The user first loads a page.
isLoading
is true because the data was never loaded, andisFetching
is also true because data is being fetched. - The dataProvider returns the data. Both
isLoading
andisFetching
become false - The user navigates away
- The user comes back to the first page, which triggers a new fetch.
isLoading
is false, because the stale data is available, andisFetching
is true because data is being fetched via the dataProvider. - The dataProvider returns the data. Both
isLoading
andisFetching
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.
import { useGetOne, useRecordContext } from 'react-admin';
const UserProfile = () => {
const record = useRecordContext();
const { data, isLoading, error } = useGetOne('users', { id: record.id });
if (isLoading) { return <Loading />; }
if (error) { return <p>ERROR</p>; }
return <div>User {data.username}</div>;
};
As a consequence, you should always use isLoading
to determine if you need to show a loading indicator.
Calling Custom Methods
Admin interfaces often have to query the API beyond CRUD requests. For instance, a user profile page may need to get the User object based on a user id. Or, users may want to “Approve” a comment by pressing a button, and this action should update the is_approved
property and save the updated record in one click.
Your dataProvider may contain custom methods, e.g. for calling RPC endpoints on your API. useQuery
and useMutation
are especially useful for calling these methods.
For instance, if your dataProvider
exposes a banUser()
method:
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 <BanUser>
button component as follows:
const BanUserButton = ({ userId }) => {
const dataProvider = useDataProvider();
const { mutate, isLoading } = useMutation(
() => dataProvider.banUser(userId)
);
return <Button label="Ban" onClick={() => 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:
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:
import { useGetOne, useRecordContext } from 'react-admin';
const UserProfile = () => {
const record = useRecordContext();
const { data, isLoading, error } = useGetOne(
'users',
{ id: record.id },
{ onSettled: (data, error) => console.log(data, error) }
);
if (isLoading) { return <Loading />; }
if (error) { return <p>ERROR</p>; }
return <div>User {data.username}</div>;
};
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 <List>
component, you can do the following:
import { List, Datagrid, TextField } from 'react-admin';
const PostList = () => (
<List
queryOptions={{ onSettled: (data, error) => console.log(data, error) }}
>
<Datagrid>
<TextField source="id" />
<TextField source="title" />
<TextField source="body" />
</Datagrid>
</List>
);
Synchronizing Dependent Queries
All Data Provider 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:
// fetch posts
const { data: posts, isLoading } = useGetList(
'posts',
{ pagination: { page: 1, perPage: 20 }, sort: { field: 'name', order: 'ASC' } },
);
// then fetch categories for these posts
const { data: categories, isLoading: isLoadingCategories } = useGetMany(
'categories',
{ ids: posts.map(post => posts.category_id) },
// run only if the first query returns non-empty result
{ enabled: !isLoading && posts.length > 0 }
);
Success and Error Side Effects
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.
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 <ApproveButton>
that notifies the user of success or failure using the bottom notification banner:
import * as React from 'react';
import { useUpdate, useNotify, useRedirect, useRecordContext, Button } from 'react-admin';
const ApproveButton = () => {
const record = useRecordContext();
const notify = useNotify();
const redirect = useRedirect();
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: 'error' });
},
}
);
return <Button label="Approve" onClick={() => approve()} disabled={isLoading} />;
};
React-admin provides the following hooks to handle the most common side effects:
useNotify
: Return a function to display a notification.useRedirect
: Return a function to redirect the user to another page.useRefresh
: Return a function to force a rerender of the current view (equivalent to pressing the Refresh button).useUnselect
: Return a function to unselect lines in the currentDatagrid
based on the ids passed to it.useUnselectAll
: Return a function to unselect all lines in the currentDatagrid
.
Optimistic Rendering and Undo
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.
import * as React from 'react';
import { useUpdate, useNotify, useRedirect, useRecordContext, Button } from 'react-admin';
const ApproveButton = () => {
const record = useRecordContext();
const notify = useNotify();
const redirect = useRedirect();
const [approve, { isLoading }] = useUpdate(
'comments',
{ id: record.id, data: { isApproved: true } },
{
onSuccess: (data) => {
redirect('/comments');
notify('Comment approved');
},
onError: (error) => {
notify(`Comment approval error: ${error.message}`, { type: 'error' });
},
}
);
return <Button label="Approve" onClick={() => approve()} disabled={isLoading} />;
};
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 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 react-query cache 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.
Here is a quick recap of the three mutation modes:
pessimistic | optimistic | undoable | |
---|---|---|---|
dataProvider call | immediate | immediate | delayed |
local changes | when dataProvider returns | immediate | immediate |
side effects | when dataProvider returns | immediate | immediate |
cancellable | no | no | yes |
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 useUpdate
hook, too. You just need to pass a mutationMode
option:
import * as React from 'react';
import { useUpdate, useNotify, useRedirect, useRecordContext, Button } from 'react-admin';
const ApproveButton = () => {
const record = useRecordContext();
const notify = useNotify();
const redirect = useRedirect();
const [approve, { isLoading }] = useUpdate(
'comments',
{ id: record.id, data: { isApproved: true } }
{
+ mutationMode: 'undoable',
- onSuccess: (data) => {
+ onSuccess: () => {
redirect('/comments');
- notify('Comment approved');
+ notify('Comment approved', { undoable: true });
},
onError: (error) => notify(`Error: ${error.message}`, { type: 'error' }),
}
);
return <Button label="Approve" onClick={() => approve()} disabled={isLoading} />;
};
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.
The following hooks accept a mutationMode
option:
Querying The API With fetch
Data Provider method hooks 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:
// in src/comments/ApproveButton.js
import * as React from 'react';
import { useState } from 'react';
import { useNotify, useRedirect, useRecordContext, Button } from 'react-admin';
const ApproveButton = () => {
const record = useRecordContext();
const redirect = useRedirect();
const notify = useNotify();
const [loading, setLoading] = useState(false);
const handleClick = () => {
setLoading(true);
const updatedRecord = { ...record, is_approved: true };
fetch(`/comments/${record.id}`, { method: 'PUT', body: updatedRecord })
.then(() => {
notify('Comment approved');
redirect('/comments');
})
.catch((e) => {
notify('Error: comment not approved', { type: 'error' })
})
.finally(() => {
setLoading(false);
});
};
return <Button label="Approve" onClick={handleClick} disabled={loading} />;
};
export default ApproveButton;
Tip: APIs often require a bit of HTTP plumbing to deal with authentication, query parameters, encoding, headers, etc. It turns out you probably already have a function that maps from a REST request to an HTTP request: your Data Provider. So it’s often better to use useDataProvider
instead of fetch
.