React Admin v3: Zoom in the Data Layer
React-admin v3, currently in beta, will soon be released as stable. It doesn't bring many new features, but focuses on improving the developer experience thanks to React hooks. And the most dramatic changes concern the way developers query the dataProvider
- that is, how they interact with their API.
Let's see these differences in detail through a few concrete examples, including a focus on optimistic rendering and side effects.
useQuery
and useMutation
Hooks
The Query
and Mutation
components introduced in react-admin 2.8 made it easier to call the dataProvider
. For instance, here is how to query the API for a user record in order to display a user profile:
import { Query } from 'react-admin';
const UserProfile = ({ record }) => (
<Query
type="GET_ONE"
resource="users"
payload={{ id: record.userId }}
>
{({ data, loading, error }) =>
loading ? <Loading />
: error ? <Error />
: (
<ul>
<li>Name: {data.name}</li>
<li>Email: {data.email}</li>
</ul>
);
}
</Query>
);
But the idiomatic way to call API endpoints in React 16.8 is hooks. So react-admin v3 provides hook alternatives to the Query
and Mutation
components, called useQuery
and useMutation
:
import { useQuery } from "react-admin";
const UserProfile = ({ userId }) => {
const { data, loading, error } = useQuery({
type: "getOne",
resource: "users",
payload: { id: userId },
});
if (loading) return <Loading />;
if (error) return <Error />;
if (!data) return null;
return (
<ul>
<li>Name: {data.name}</li>
<li>Email: {data.email}</li>
</ul>
);
};
Apart from the move from render props to hooks, there is a subtle difference between the v2 and v3 examples: the query type was GET_ONE
in v2, it's getOne
in v3. We'll explain why later in this article.
Optimistic Rendering For the Masses
useQuery
calls the dataProvider
when the component mounts, and displays the data when the response arrives. But most of the time, react-admin doesn't need to call the dataProvider
to get data: it can just grab it from the Redux store. That's what the react-admin List
and Edit
component do: they use optimistic rendering, which means they render data from the Redux store before the result from the dataProvider
arrives. In many cases, this means the result displays immediately.
To get the same functionality with react-admin v2, you had to dispatch a specific Redux action, and connect to an undocumented location in the Redux store to grab the result. In react-admin v3, it's as easy as replacing useQuery
by useQueryWithStore
, which has exactly the same API:
-import { useQuery } from 'react-admin';
+import { useQueryWithStore } from 'react-admin';
const UserProfile = ({ userId }) => {
- const { data, loading, error } = useQuery({
+ const { data, loading, error } = useQueryWithStore({
type: 'getOne',
resource: 'users',
payload: { id: userId }
});
if (loading) return <Loading />;
if (error) return <Error />
if (!data) return null;
return (
<ul>
<li>Name: {data.name}</li>
<li>Email: {data.email}</li>
</ul>
)
}
The return object of useQuery
and useQueryWithStore
contains both a loading
and a loaded
field. For useQuery
, these two fields are opposite of one another. but for useQueryWithStore
, there is a slight difference. After the first mount, the data is already in the store, so loaded
is always true
. That means it's better to use loaded
than loading
when using optimistic rendering - it hides the loader on subsequent mounts.
import { useQueryWithStore } from 'react-admin';
const UserProfile = ({ userId }) => {
- const { data, loading, error } = useQueryWithStore({
+ const { data, loaded, error } = useQueryWithStore({
type: 'getOne',
resource: 'users',
payload: { id: userId }
});
- if (loading) return <Loading />;
+ if (!loaded) return <Loading />;
if (error) return <Error />
if (!data) return null;
return (
<ul>
<li>Name: {data.name}</li>
<li>Email: {data.email}</li>
</ul>
)
}
To render optimistically, the <List>
and <Edit>
components use the useQueryWithStore
hook to call the dataProvider
. You can use this new hook with confidence: it's one of the pillars of the react-admin rendering model.
Specialized Data Provider Hooks
useQueryWithStore
lets you call any verb on the dataProvider
, and therefore has a generic signature. React-admin v3 also offers specialized hooks for each dataProvider
verb: useGetList
for GET_LIST
, useGetOne
for GET_ONE
, etc. Here is how to use useGetOne
for the UserProfile
component:
-import { useQueryWithStore } from 'react-admin';
+import { useGetOne } from 'react-admin';
const UserProfile = ({ userId }) => {
- const { data, loaded, error } = useQueryWithStore({
- type: 'getOne',
- resource: 'users',
- payload: { id: userId }
- });
+ const { data, loaded, error } = useGetOne('users', userId);
if (!loaded) return <Loading />;
if (error) return <Error />
if (!data) return null;
return (
<ul>
<li>Name: {data.name}</li>
<li>Email: {data.email}</li>
</ul>
)
}
There are two benefits to these specialized dataProvider
hooks:
- they's shorter to write (and more expressive)
- they allow compile-time argument validation (using TypeScript) and therefore help preventing errors
We recommend using them as much as possible.
Although react-admin is written mostly in TypeScript, applications using react-admin can use TypeScript or JavaScript. You don't have to use TypeScript to use react-admin v3!
Side Effects As Functions
In react-admin v2, when you wanted to do something special after the response arrives, you had to use a side effect object in onSuccess
and onFailure
options. For instance, to display a notification and redirect the user to another page after a mutation, you would do the following:
import { Mutation } from "react-admin";
const ApproveButton = ({ record }) => {
return (
<Mutation
type="UPDATE"
resource="comments"
payload={{ id: record.id, data: { is_approved: true } }}
options={{
onSuccess: {
notification: { body: "Comment approved" },
redirectTo: "/comments",
},
onFailure: {
notification: {
body: "Error: comment not approved",
level: "warning",
},
},
}}
>
{(approve, { loading }) => (
<FlatButton label="Approve" onClick={approve} disabled={loading} />
)}
</Mutation>
);
};
export default ApproveButton;
React-admin had to use redux-saga to handle these side effects, which made the implementation kind of cryptic for most people. Adding your own side effects was very hard, too. And finally, it was necessary to look at the documentation very often to know how a side effect was named.
This lead us to transform onSuccess
and onFailure
options to functions. That way, side effects can be anything you want - and usually, they are a callback returned by a hook.
import { useMutation, useNotify, useRedirect } from "react-admin";
const ApproveButton = ({ record }) => {
const notify = useNotify();
const redirectTo = useRedirect();
const [approve, { loading }] = useMutation(
{
type: "update",
resource: "comments",
payload: { id: record.id, data: { is_approved: true } },
},
{
onSuccess: ({ data }) => {
notify(`Comment #${data.id} approved`);
redirectTo("/comments");
},
onFailure: error => {
notify(`Comment approval error: ${error.message}`, "warning");
},
}
);
return <FlatButton label="Approve" onClick={approve} disabled={loading} />;
};
Using a function instead of an object allows side effects to be based on the data provider response (or the error it throw in case of failure). It is trivial to add custom side effects. We also think that the code is more readable that way.
In addition to useNotify
and useRedirect
, which are illustrated above, react-admin v3 provides 2 more side effect hooks: useRefresh
and useUnselectAll
.
New Data Provider Signature
React-admin v2 relies on Data Provider verbs: constants like GET_LIST
, GET_ONE
, etc. Data Providers use these verbs to determine the query they're supposed to run.
But with the rise of specialized hooks like useGetOne
, and the possibility to detect bugs at compile time using TypeScript, these verbs were limiting. In react-admin v3, the query verbs are now methods that you can call on the dataProvider
, because it's an object.
const dataProvider = {
getList: (resource, params) => Promise,
getOne: (resource, params) => Promise,
getMany: (resource, params) => Promise,
getManyReference: (resource, params) => Promise,
create: (resource, params) => Promise,
update: (resource, params) => Promise,
updateMany: (resource, params) => Promise,
delete: (resource, params) => Promise,
deleteMany: (resource, params) => Promise,
};
That explains why the useQuery
hook uses the getOne
query type instead of GET_ONE
.
React-admin v3 also bundles sophisticated TypeScript interfaces for the return type of each dataProvider
method, that will allow you to detect subtle bugs at compile time.
If you have written a custom dataProvider
for react-admin v2, it will still work with react-admin v3, because we've included a wrapper for legacy Data Providers. No need for a long rewrite.
Conclusion
It took us a long time to find the right API for Data Provider hooks. In fact, while working on react-admin v3, we've rewritten these hooks twice. But once we've started using the new API, we were confident that it improved the developer experience a great deal. We've even reached a point where we're using the Data Provider logic from react-admin in non react-admin projects.
We think you're going to love these hooks, too. To try them early on, upgrade to react-admin v3.